Forms & Submissions
Sthalam enables publishers to collect viewer feedback through multiple independent forms embedded in websites. A single document can have multiple forms (e.g., contact form, feedback form, survey), each differentiated by form_id. Using the append-only permission model (crud/append), viewers can submit form data without seeing other viewers’ submissions, preserving privacy while enabling data collection. Forms render from local copies first, then apply incremental CRDT updates in the background.
Form Submission Flow
Section titled “Form Submission Flow” 
Viewer Experience
Section titled “Viewer Experience”Step 1: Opening Website (Local-First)
Section titled “Step 1: Opening Website (Local-First)”- Load Local First: Website renders instantly from local blocksuite_doccopy
- Display Forms: All form blocks (contact_form, feedback_form, etc.) render immediately
- Background Poll: Check node for incremental updates in background
- Apply Updates: If forms changed, apply only incremental CRDT diffs
- UCAN Permission: crud/appendforform_submissions_doc
Key Benefit: Instant load, no waiting for network
Step 2: Filling Form
Section titled “Step 2: Filling Form”User Experience:
- Interactive form fields
- Form submission with validation
- Confirmation on success
Step 3: Submitting Data to Specific Form
Section titled “Step 3: Submitting Data to Specific Form”When viewer clicks submit on a specific form (e.g., contact_form):
// Create submission record with form_id and field metadataconst formId = "contact_form"; // From form block's block_id
const submission = {  id: generateUUID(),  form_id: formId, // Identifies which form this submission is for  timestamp: Date.now(),  viewer_id: viewerPublicKey,  fields: {    name: {      value: "Alice Viewer",      metadata: { parse_as: "contact_name", validation: "min_length:2" }    },    email: {      value: "alice@example.com",      metadata: { parse_as: "contact_email", validation: "email_format" }    },    message: {      value: "Great content!",      metadata: { parse_as: "contact_message", max_length: 1000 }    },    urgent: {      value: true,      metadata: { parse_as: "is_urgent" }    }  }};
// Add to Yjs doc (organized by form_id)const submissionsDoc = formSubmissionsDoc;const submissionsMap = submissionsDoc.getMap('forms'); // Organized by form_idconst formArray = submissionsMap.get(formId) || submissionsDoc.getArray(formId);if (!submissionsMap.has(formId)) {  submissionsMap.set(formId, formArray);}formArray.push([submission]);
// Create incremental Yjs CRDT update (only the diff, not full doc)const update = Y.encodeStateAsUpdate(submissionsDoc);
// Send to node via ViewerCommentsUpdateawait sendMessage({  type: "ViewerCommentsUpdate",  resource_id: resourceId,  resource_ucan: resourceUcanToken,  // Contains crud/append permission  sync_data: base64Encode(update)});Append-Only Sync Model
Section titled “Append-Only Sync Model”Permission: crud/append
Section titled “Permission: crud/append”The viewer’s UCAN token for form_submissions_doc contains:
sthalam:resource:abc123:form_submissions_doc - crud/appendWhat crud/append Means:
- Can append new records to the document
- Cannot read existing submissions
- Cannot update existing submissions
- Cannot delete submissions
Node Processing
Section titled “Node Processing”When node receives ViewerCommentsUpdate with form submission:
Validation (network/src/p2p/website_handler.rs:866-963):
// 1. Validate resource UCAN structure and signaturelet ucan = crypto_utils::ucan_utils::validate_structure(&resource_ucan).await?;
// 2. Extract and verify resource_id matcheslet ucan_resource_id = extract_resource_id_from_ucan(&ucan)?;assert_eq!(ucan_resource_id, resource_id);
// 3. Check capability (implicitly requires crud/append for form_submissions_doc)// UCAN token must contain "sthalam:resource:{id}:form_submissions_doc" with "crud/append"
// 4. Apply append operation to form_submissions_docservices::apply_updates(    &resource_id,    &sync_data,  // Yjs CRDT update with submission    &local_user.id,    repo_ctx,    &crypto_utils,).await?;Storage:
- Node stores incremental CRDT update in form_submissions_doc
- All forms’ submissions in single doc, organized by form_id
- Submission encrypted with resource AES key
- Indexed for publisher retrieval
- Not sent back to viewer
Publisher Sync (Local-First + Incremental)
Section titled “Publisher Sync (Local-First + Incremental)”When publisher views submissions:
- Load Local First: Publisher opens submissions viewer, renders from local form_submissions_docinstantly
- Background Poll: Poll node for new submissions (IncrementalSyncRequest)
- Receive Only Diffs: Node sends only incremental CRDT updates, not full doc
- Apply Updates: Apply incremental diffs to local copy
- Decrypt Data: Publisher decrypts new submissions with resource AES key
- View by form_id: Display submissions grouped by form (contact_form, feedback_form, etc.)
- Parse Fields: Use field metadata (parse_as) for structured display
Key Benefits:
- Instant load from local copy
- Efficient incremental sync
- Only new submissions transferred
- Forms organized by form_id
Privacy Properties
Section titled “Privacy Properties”Viewer Isolation
Section titled “Viewer Isolation”Viewers Cannot See Other Submissions:
- crud/appendpermission doesn’t include read access
- Each viewer only knows their own submission
- No cross-viewer data leakage
- Privacy preserved by permission model
Publisher Access:
- Publisher has full crud/readpermission forform_submissions_doc
- Can view all submissions
- Can export and analyze data
- Full ownership and control
Encryption
Section titled “Encryption”Data at Rest:
- Submissions encrypted with resource AES key
- Stored in node’s encrypted database
- Only publisher can decrypt
Data in Transit:
- QUIC connection encrypts transport
- AES key encrypted per recipient (viewer/publisher) - later releases will generate new AES key per viewer
- End-to-end encryption maintained
Publisher’s Submissions Viewer
Section titled “Publisher’s Submissions Viewer”Viewing Submissions (Multi-Form Support)
Section titled “Viewing Submissions (Multi-Form Support)”Publisher interface for reviewing form responses:
Submission List by Form:
- Group by form_id: Separate views for contact_form, feedback_form, etc.
- Timestamp of each submission
- Viewer identifier (public key or pseudonym)
- Field values with metadata: Display using parse_asfor structured presentation- contact_email (parsed as email)
- experience_rating (parsed as satisfaction category)
- is_urgent (parsed as boolean flag)
 
- Sort and filter capabilities per form
- Cross-form analytics: Compare submissions across different forms
Data Export:
- Export by form_id (CSV/JSON for specific form)
- Export all forms combined
- Field metadata included for parsing
- Analyze responses with custom tools
- Integration with analytics platforms
No Modification:
- Append-only model means submissions are immutable
- Preserves integrity of viewer responses
- Cryptographic audit trail
- Full history maintained
Use Cases
Section titled “Use Cases”Contact Forms
Section titled “Contact Forms”Collect messages from website visitors:
- Name, email, message
- Category/topic selection
- Privacy-preserving (viewers don’t see others’ messages)
Surveys & Polls
Section titled “Surveys & Polls”Gather feedback and opinions:
- Multiple choice questions
- Rating scales
- Open-ended responses
- Aggregate without exposing individual responses to other viewers
Event Registration
Section titled “Event Registration”Collect RSVPs and attendee information:
- Name and contact details
- Dietary restrictions
- Number of guests
- Privacy for attendees
Bug Reports & Feature Requests
Section titled “Bug Reports & Feature Requests”Collect user feedback:
- Description of issue/idea
- Priority/severity
- Contact for follow-up
- Organized feedback collection
Technical Implementation
Section titled “Technical Implementation”Yjs CRDT for Multiple Forms
Section titled “Yjs CRDT for Multiple Forms”Form submissions use Yjs Map structure organized by form_id:
// Initialize form_submissions_docconst formSubmissionsDoc = new Y.Doc();const formsMap = formSubmissionsDoc.getMap('forms'); // Organized by form_id
// Add submission to specific form (append operation)const formId = "contact_form"; // Or "feedback_form", etc.const submissionId = generateUUID();
const submissionData = {  id: submissionId,  form_id: formId,  timestamp: Date.now(),  viewer_id: viewerPublicKey,  fields: {    name: {      value: "Alice Viewer",      metadata: { parse_as: "contact_name", validation: "min_length:2" }    },    email: {      value: "alice@example.com",      metadata: { parse_as: "contact_email", validation: "email_format" }    },    // ... more fields with metadata  }};
// Get or create array for this form_idconst formArray = formsMap.get(formId) || formSubmissionsDoc.getArray(formId);if (!formsMap.has(formId)) {  formsMap.set(formId, formArray);}
// Append submission to this form's arrayformArray.push([submissionData]);
// Generate incremental Yjs CRDT update (only the diff)const update = Y.encodeStateAsUpdate(formSubmissionsDoc);Data Structure:
// form_submissions_doc structure{  forms: YMap {    "contact_form": YArray [      { id: "s1", form_id: "contact_form", fields: {...}, ... },      { id: "s2", form_id: "contact_form", fields: {...}, ... }    ],    "feedback_form": YArray [      { id: "s3", form_id: "feedback_form", fields: {...}, ... },      { id: "s4", form_id: "feedback_form", fields: {...}, ... }    ]  }}CRDT Properties:
- Append operations commute (order-independent)
- Concurrent submissions to different forms don’t conflict
- Concurrent submissions to same form don’t conflict
- Automatic conflict resolution
- Guaranteed eventual consistency
- Single doc for all forms, organized by form_id
State Vector Limitation
Section titled “State Vector Limitation”Viewers do not send state vectors for form_submissions_doc:
Why?:
- crud/appenddoesn’t include read access
- Viewer shouldn’t know what’s in the document
- Only sends new append operations
Implementation:
- Viewer creates submission
- Encodes as Yjs update
- Sends raw update without state vector exchange
- Node applies update blindly (after UCAN validation)
Security Considerations
Section titled “Security Considerations”Spam Prevention:
- Rate limiting on node side
- UCAN token expiration
- Viewer identity tracking
Data Validation:
- UCAN token validation (see network/src/p2p/website_handler.rs:867-963)
- Publisher reviews submissions
- Server-side field validation
- Malicious input filtering
Privacy Compliance:
- Viewers cannot access others’ data
- Publisher controls all submission data
- GDPR/privacy law compliance possible
What Happens After Submission
Section titled “What Happens After Submission”- Viewer Submits: Form data encoded as Yjs update
- Node Validates: UCAN token checked for crud/appendpermission
- Node Stores: Update applied to form_submissions_doc
- Publisher Syncs: Receives submission during next sync
- Publisher Reviews: Views submission in submissions viewer interface
- No Feedback to Viewer: Append-only means no updates flow back
The append-only permission model enables privacy-preserving form submissions—viewers can contribute data without seeing others’ responses, while publishers maintain full control and visibility over all collected data.