Home Doh Ref
Dohballs
  • 📁 doh_chat
  • 📁 doh_modules
    • 📦 dataforge
    • 📦 express
    • 📁 sso
    • 📁 user

Collaborative Documents

Real-time collaborative document editing for doh_chat using Yjs CRDTs.

Features

  • Persistent Room Documents: Documents attached to chat rooms persist across sessions
  • Lobby Welcome Doc: Automatic lobby document created on server startup
  • Real-time Collaboration: Multiple users can edit simultaneously using Yjs CRDT
  • Monaco Editor: Full-featured code editor with syntax highlighting
  • Permission System: Owner/editor/viewer roles with room-based access control

Architecture

Server (collab_docs_server.js)

  • Yjs Document Management: In-memory active documents with periodic persistence
  • Socket.IO Protocol: Uses y-protocols for sync and awareness
  • Database Storage: SQLite via AsyncDataforge
    • collab_docs.documents - Document metadata
    • collab_docs.doc_state - Yjs CRDT state (base64 encoded)
  • Permission Groups:
    • collab_doc_owner - Full control (read, write, delete, share)
    • collab_doc_editor - Read and write access
    • collab_doc_viewer - Read-only access
    • collab_doc_room_member - Room members can read/write room docs

Client (collab_docs_client.js)

Three main patterns:

  • CollabDocManager - Full-page document list and editor
  • CollabDocManagerToolbar - Compact toolbar with doc tiles (used in chat)
  • CollabDocManagerEditor - Monaco editor with Yjs binding

Lobby Documents

On server startup, a "Lobby Welcome" document is automatically created if it doesn't exist:

  • Document ID: lobby-welcome-doc
  • Room ID: lobby
  • Owner: system
  • Pre-populated with welcome markdown

All authenticated users can read and edit the lobby document.

Room Documents

Documents can be attached to chat rooms via room_id:

  • Only visible to room members
  • Persist across sessions
  • Loaded automatically when entering a room
  • Real-time sync between all room members

Permission Flow

  1. Room Member Resolution:

    • Lobby: Returns ['*'] wildcard (all authenticated users)
    • Rooms: Queries chat.room_members table
  2. Access Check:

    • Builds permission context with document owner, editors, viewers, and room members
    • Evaluates via Doh.permit() against permission groups
    • Checks cascade: owner → editor → room member → viewer
  3. Document Listing:

    • Pre-filters standalone docs to user's owned/shared docs
    • Room-attached docs checked via full permission system
    • Returns only accessible documents

Usage in Chat

The chat module loads collab_docs package, which includes both server and client modules.

When a user enters a room:

  1. chat_client.js calls docToolbar.loadDocs()
  2. Client emits collab:doc:list with room_id
  3. Server queries all docs, filters by room and permissions
  4. Client renders doc tiles in the toolbar
  5. Clicking a tile opens the Monaco editor

Socket Events

Document Listing

  • Event: collab:doc:list
  • Request: { room_id: string | null }
  • Response: { docs: Array<Document> }

Document Operations

  • collab:doc:create - Create new document
  • collab:doc:update_meta - Update title/language
  • collab:doc:delete - Delete document
  • collab:doc:join - Join document for editing
  • collab:doc:leave - Leave document
  • collab:doc:close - Close document

Sync Protocol

  • collab:doc:sync - Yjs sync messages
  • collab:doc:awareness - Cursor/presence updates
  • collab:doc:update - Broadcast document updates
  • collab:doc:client_count - Active client count updates

Important Notes

Client-Side API Calls

⚠️ Always use socket.emit('collab:doc:list', ...) for listing documents, not Doh.ajaxPromise().

Why: ajaxPromise() with a URL attempts to route through socket.io, which can trigger the REST POST handler for document creation instead of the GET list handler. Using socket.emit directly ensures the correct event handler is called.

// ✅ CORRECT
Doh.socket.emit('collab:doc:list', { room_id: this.room_id }, (response) => {
  if (response?.docs) {
    this.renderTiles(response.docs);
  }
});

// ❌ WRONG - May trigger document creation instead of listing
const result = await Doh.ajaxPromise('/api/collab/docs', { room_id: this.room_id });

Database Schema

documents table (Idea table)

{
  id: string,              // UUID
  title: string,
  room_id: string | null,  // Chat room ID or null for standalone
  owner: string,           // Username
  editors: string[],       // Array of usernames
  viewers: string[],       // Array of usernames
  type: string,            // 'plaintext' | 'richtext'
  language: string,        // Monaco language ID
  created_at: number,      // Timestamp
  updated_at: number       // Timestamp
}

doc_state table (Idea table)

{
  id: string,      // Document ID
  state: string    // Base64-encoded Yjs state
}
Last updated: 2/9/2026