Skip to content

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 diagram showing form design, viewer filling form, append-only sync, and publisher reviewing submissions
  1. Load Local First: Website renders instantly from local blocksuite_doc copy
  2. Display Forms: All form blocks (contact_form, feedback_form, etc.) render immediately
  3. Background Poll: Check node for incremental updates in background
  4. Apply Updates: If forms changed, apply only incremental CRDT diffs
  5. UCAN Permission: crud/append for form_submissions_doc

Key Benefit: Instant load, no waiting for network

User Experience:

  • Interactive form fields
  • Form submission with validation
  • Confirmation on success

When viewer clicks submit on a specific form (e.g., contact_form):

// Create submission record with form_id and field metadata
const 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_id
const 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 ViewerCommentsUpdate
await sendMessage({
type: "ViewerCommentsUpdate",
resource_id: resourceId,
resource_ucan: resourceUcanToken, // Contains crud/append permission
sync_data: base64Encode(update)
});

The viewer’s UCAN token for form_submissions_doc contains:

sthalam:resource:abc123:form_submissions_doc - crud/append

What crud/append Means:

  • Can append new records to the document
  • Cannot read existing submissions
  • Cannot update existing submissions
  • Cannot delete submissions

When node receives ViewerCommentsUpdate with form submission:

Validation (network/src/p2p/website_handler.rs:866-963):

// 1. Validate resource UCAN structure and signature
let ucan = crypto_utils::ucan_utils::validate_structure(&resource_ucan).await?;
// 2. Extract and verify resource_id matches
let 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_doc
services::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:

  1. Load Local First: Publisher opens submissions viewer, renders from local form_submissions_doc instantly
  2. Background Poll: Poll node for new submissions (IncrementalSyncRequest)
  3. Receive Only Diffs: Node sends only incremental CRDT updates, not full doc
  4. Apply Updates: Apply incremental diffs to local copy
  5. Decrypt Data: Publisher decrypts new submissions with resource AES key
  6. View by form_id: Display submissions grouped by form (contact_form, feedback_form, etc.)
  7. 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

Viewers Cannot See Other Submissions:

  • crud/append permission 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/read permission for form_submissions_doc
  • Can view all submissions
  • Can export and analyze data
  • Full ownership and control

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 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_as for 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

Collect messages from website visitors:

  • Name, email, message
  • Category/topic selection
  • Privacy-preserving (viewers don’t see others’ messages)

Gather feedback and opinions:

  • Multiple choice questions
  • Rating scales
  • Open-ended responses
  • Aggregate without exposing individual responses to other viewers

Collect RSVPs and attendee information:

  • Name and contact details
  • Dietary restrictions
  • Number of guests
  • Privacy for attendees

Collect user feedback:

  • Description of issue/idea
  • Priority/severity
  • Contact for follow-up
  • Organized feedback collection

Form submissions use Yjs Map structure organized by form_id:

// Initialize form_submissions_doc
const 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_id
const formArray = formsMap.get(formId) || formSubmissionsDoc.getArray(formId);
if (!formsMap.has(formId)) {
formsMap.set(formId, formArray);
}
// Append submission to this form's array
formArray.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

Viewers do not send state vectors for form_submissions_doc:

Why?:

  • crud/append doesn’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)

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
  1. Viewer Submits: Form data encoded as Yjs update
  2. Node Validates: UCAN token checked for crud/append permission
  3. Node Stores: Update applied to form_submissions_doc
  4. Publisher Syncs: Receives submission during next sync
  5. Publisher Reviews: Views submission in submissions viewer interface
  6. 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.