Website Synchronization
Sthalam’s website synchronization protocol ensures viewers stay up-to-date with publisher content while maintaining efficient network usage and respecting permission boundaries. The sync protocol differs significantly from Livnote’s user synchronization, focusing on content distribution rather than collaborative editing.
Synchronization Types
Section titled “Synchronization Types”Initial Sync
Section titled “Initial Sync”First-time connection when viewer subscribes to publisher:
- Complete folder and resource transfer
- All existing content delivered
- Viewer marked as
first_sync = true
Incremental Sync
Section titled “Incremental Sync”Subsequent connections to receive updates:
- Detect new resources added to folder
- Sync changes to existing resources
- Efficient state vector-based reconciliation
User Sync vs. Website Sync
Section titled “User Sync vs. Website Sync”User Sync (Livnote - Publisher ↔ Node)
Section titled “User Sync (Livnote - Publisher ↔ Node)”Purpose: Synchronize shared user context between collaborating peers
Flow:
- Publisher and node exchange manifests
- Compare known users and devices
- Share delegated connection tokens
- Enable transitive peer discovery
Use Case: Building peer-to-peer collaboration networks
Website Sync (Sthalam - Node ↔ Viewer)
Section titled “Website Sync (Sthalam - Node ↔ Viewer)”Purpose: Distribute published content to viewers with specific permissions
Flow:
- Viewer sends resource manifest
- Node detects new/updated resources
- Node validates UCAN permissions
- Sends content with granular permissions
Use Case: Sovereign content publishing and distribution
Incremental Sync Protocol
Section titled “Incremental Sync Protocol”
Step 1: Viewer Reconnection
Section titled “Step 1: Viewer Reconnection”When a viewer reconnects (e.g., comes back online):
- Handshake: WebsiteHandshake exchange to verify identities
- Check First Sync: Database query shows
first_sync = true - Trigger Incremental Sync: Call
perform_incremental_sync()
Step 2: Folder Resource Info Exchange
Section titled “Step 2: Folder Resource Info Exchange”Viewer Builds Manifest (network/src/p2p/website_handler.rs:426-512):
// Get all folders viewer has access tolet folder_manifest = services::get_viewer_folder_manifest( &local_user.id, repo_ctx).await?;
// For each folderfor folder_info in folder_manifest { // Send folder ID + resource IDs + folder UCAN let message = Message::Website(WebsiteMessage::FolderResourceInfo { folder_id: folder_info.folder_id, resource_ids: folder_info.resource_ids, // What viewer currently has folder_ucan: folder_info.folder_ucan, });}Node Validates and Compares (network/src/p2p/website_handler.rs:514-669):
// 1. Validate folder UCANlet ucan = crypto_utils::ucan_utils::validate_structure(&folder_ucan).await?;
// 2. Extract and verify folder_idlet ucan_folder_id = crypto_utils::ucan_utils::extract_folder_id_from_ucan(&ucan)?;assert_eq!(ucan_folder_id, folder_id);
// 3. Get node's resource list for this folderlet node_resource_ids = repo_ctx .resource_repo .get_resource_ids_by_folder_id(&folder_id) .await?;
// 4. Find new resources (on node but not on viewer)let new_resource_ids: Vec<String> = node_resource_ids .into_iter() .filter(|id| !viewer_resource_ids.contains(id)) .collect();Step 3: New Resource Distribution
Section titled “Step 3: New Resource Distribution”For each new resource the viewer doesn’t have:
Resource Preparation:
// Get resource typelet resource = repo_ctx .resource_repo .find_by_id(&resource_id, &local_user.id) .await?;
// Prepare for viewer with resource-specific permissionslet resource_sync_data = services::prepare_resource_for_viewer( &resource_id, &resource.resource_type, &viewer_user, &local_user, repo_ctx, &crypto_utils,).await?;Permission Generation (services/src/node_service.rs:217-234):
// Generate 3-document permissionslet permissions = vec![ ( format!("sthalam:resource:{}:blocksuite_doc", resource_id), "crud/read".to_string(), // Main content: read-only ), ( format!("sthalam:resource:{}:thread_comments_doc", resource_id), "crud/write".to_string(), // Comments: bidirectional ), ( format!("sthalam:resource:{}:form_submissions_doc", resource_id), "crud/append".to_string(), // Submissions: append-only ),];Distribution:
// Send new resource with viewer-specific UCANself.send_message(Message::ResourceAdditionRequest(resource_sync_data)).await?;Step 4: Existing Resource Sync
Section titled “Step 4: Existing Resource Sync”For resources viewer already has, sync state changes:
Viewer Sends State Vectors:
// Get Yjs state vectors for each documentlet sync_info = services::get_resource_sync_info( &resource_id, &local_user.id, repo_ctx, &crypto_utils,).await?;
// Send incremental sync requestlet message = Message::Website(WebsiteMessage::IncrementalSyncRequest { resource_id: sync_info.resource_id, resource_ucan: sync_info.resource_ucan, // Resource-specific UCAN sync_data: sync_info.sync_data, // Yjs state vectors});Node Validates and Processes (network/src/p2p/website_handler.rs:673-770):
// 1. Validate resource UCANlet ucan = crypto_utils::ucan_utils::validate_structure(&resource_ucan).await?;
// 2. Extract and verify resource_idlet ucan_resource_id = crypto_utils::ucan_utils::extract_resource_id_from_ucan(&ucan)?;assert_eq!(ucan_resource_id, resource_id);
// 3. Process sync and generate updateslet response_sync_data = services::process_incremental_resource_sync( &resource_id, &local_user.id, &sync_data, // Viewer's state vectors repo_ctx, &crypto_utils,).await?;
// 4. Send response with missing updatesself.send_message(Message::Website(WebsiteMessage::IncrementalSyncResponse { resource_id, sync_data: response_sync_data,})).await?;Viewer Applies Updates and Sends Comments (network/src/p2p/website_handler.rs:772-862):
// Apply node's updates to local statelet viewer_updates = services::apply_and_generate_viewer_updates( &resource_id, &local_user.id, &sync_data, // Node's updates repo_ctx, &crypto_utils,).await?;
// Emit to frontend for UI updateself.event_emitter.emit(P2PEvent::UpdatesEvent { resource_id, updates: sync_data, client_id: 0,});
// Send viewer's comment updates back to nodeself.send_message(Message::Website(WebsiteMessage::ViewerCommentsUpdate { resource_id, resource_ucan, sync_data: viewer_updates, // Only comments, not main content})).await?;Message Types
Section titled “Message Types”FolderResourceInfo
Section titled “FolderResourceInfo”Viewer sends current state:
folder_id: Which folder to syncresource_ids: List of resources viewer currently hasfolder_ucan: Folder access token for validation
IncrementalSyncRequest
Section titled “IncrementalSyncRequest”Viewer requests updates for existing resource:
resource_id: Which resource to syncresource_ucan: Resource-specific tokensync_data: Yjs state vectors (what viewer currently has)
IncrementalSyncResponse
Section titled “IncrementalSyncResponse”Node sends missing updates:
resource_id: Resource being syncedsync_data: Yjs CRDT updates (what viewer is missing)
ViewerCommentsUpdate
Section titled “ViewerCommentsUpdate”Viewer sends comment changes back:
resource_id: Resource with commentsresource_ucan: Proof of write permissionsync_data: Comment thread updates only
UCAN Token Validation
Section titled “UCAN Token Validation”Every sync message includes and validates UCAN tokens:
Folder-Level (network/src/p2p/website_handler.rs:530-555):
// Extract folder_id from UCAN capabilitieslet ucan_folder_id = extract_folder_id_from_ucan(&ucan)?;
// Verify it matches the requested folderif ucan_folder_id != folder_id { return Err("Folder ID mismatch");}Resource-Level (network/src/p2p/website_handler.rs:686-710):
// Extract resource_id from UCAN capabilitieslet ucan_resource_id = extract_resource_id_from_ucan(&ucan)?;
// Supports multiple formats:// - "domain:resource:id"// - "domain:resource:id:doc_type" (3-doc architecture)
if ucan_resource_id != resource_id { return Err("Resource ID mismatch");}This validation ensures viewers can only sync resources they have permissions for.
Sync Efficiency
Section titled “Sync Efficiency”State Vector-Based:
- Only missing CRDT operations are transferred
- No redundant data transmission
- Efficient bandwidth usage
Permission-Aware:
crud/read→ Viewer receives updates, cannot sendcrud/write→ Bidirectional sync for commentscrud/append→ Viewer can add, not modify existing
Incremental:
- Only new resources and changes are synced
- Complete re-sync not required on reconnection
- Scales efficiently with growing content libraries
Real-Time Update Flow
Section titled “Real-Time Update Flow”
When publishers create new content, viewers can receive updates by actively polling the node through the incremental sync protocol:
Publisher Creates New Content
Section titled “Publisher Creates New Content”- Create Resource: Publisher designs new website, blog post, or newsletter
- Publish to Network: Content encrypted and filed on P2P network
- Sync to Node: Publisher’s sovereign node receives the new resource via
ResourceAdditionRequest
Viewer Polls and Receives Updates
Section titled “Viewer Polls and Receives Updates”- Viewer Reconnects: Comes back online and actively initiates incremental sync
- Send Folder Manifest: Viewer sends
FolderResourceInfowith current resource list - Node Detects New Resources: Compares viewer’s list with node’s list
- Node Responds with Resources: Node includes new resources in sync response via
ResourceAdditionRequest(part of the response to viewer’s pull request) - Viewer Stores: New resources validated and stored locally
Important: The viewer initiates the sync request (pull). The node responds by including new resources in that response. The node never initiates connections or pushes data to viewers.
Key Distinction: Request vs Response
Section titled “Key Distinction: Request vs Response”- Connection: Viewer always initiates (node never initiates connections)
- New Resources: Node includes them in sync response when viewer requests sync
- Existing Resource Updates: Viewer requests via state vector sync, node responds with diffs
Viewer Access Flow
Section titled “Viewer Access Flow”Once viewer polls and receives new resources:
- Local First: Viewer opens content, loads from local database instantly
- Pull-Based Model: Viewer must actively poll node to receive future content publisher adds to folder
- No Push Notifications: Node doesn’t track viewers or send notifications - viewers control polling frequency
What Happens During Sync
Section titled “What Happens During Sync”- Viewer Initiates: Viewer reconnects and establishes connection to node
- Manifest Exchange: Viewer sends list of known resources (viewer pulls)
- New Resources: Node responds with any resources viewer doesn’t have (in sync response)
- State Reconciliation: Viewer requests updates for existing resources, node responds with Yjs state vectors
- Update Application: Both sides apply missing CRDT operations
- Comment Sync: Viewer sends comments back to node
- Completion: Viewer is fully up-to-date with publisher’s content (until next poll)
The website sync protocol enables efficient, permission-aware content distribution while maintaining the sovereignty of both publishers and viewers.