Diffdown is a real-time collaborative Markdown editor/previewer built on the AT Protocol diffdown.com

Add collaboration feature design spec

+245
+245
docs/superpowers/specs/2026-03-11-collaboration-design.md
··· 1 + # Collaboration Feature Design 2 + 3 + **Date:** 2026-03-11 4 + **Status:** Approved 5 + 6 + ## 1. Architecture Overview 7 + 8 + ``` 9 + ┌─────────────┐ WebSocket ┌─────────────┐ 10 + │ Collaborator│◄──────────────────►│ Diffdown │ 11 + │ Browser │ Auth (ATProto) │ Server │ 12 + └─────────────┘ └──────┬──────┘ 13 + 14 + ┌────────────┴────────────┐ 15 + │ │ 16 + ┌─────▼─────┐ ┌──────▼──────┐ 17 + │ WebSocket │ │ ATProto │ 18 + │ Hub │ │ PDS (Creator│ 19 + └───────────┘ └─────────────┘ 20 + ``` 21 + 22 + The Diffdown server acts as the collaboration hub — receives edits from all collaborators, maintains canonical state, pushes to Creator's PDS. 23 + 24 + ## 2. Data Model 25 + 26 + ### Updated `com.diffdown.document` 27 + 28 + ```json 29 + { 30 + "$type": "com.diffdown.document", 31 + "title": "My Doc", 32 + "content": { "$type": "at.markpub.markdown", "flavor": "gfm", "text": { "rawMarkdown": "..." } }, 33 + "textContent": "...", 34 + "collaborators": ["did:plc:abc123", "did:plc:def456"], 35 + "createdAt": "2026-03-11T...", 36 + "updatedAt": "2026-03-11T..." 37 + } 38 + ``` 39 + 40 + ### New `com.diffdown.comment` Lexicon 41 + 42 + ```json 43 + { 44 + "$type": "com.diffdown.comment", 45 + "documentURI": "at://did:plc:creator/com.diffdown.document/abc123", 46 + "paragraphId": "p-3", 47 + "text": "Great point here!", 48 + "author": "did:plc:collaborator", 49 + "createdAt": "2026-03-11T..." 50 + } 51 + ``` 52 + 53 + **Note:** The paragraph ID is generated client-side (e.g., from the ProseMirror/CodeMirror node ID). 54 + 55 + ## 3. Invite System 56 + 57 + - Creator clicks "Share" → generates invite link: `diffdown.app/doc/{rkey}?invite={token}` 58 + - Token stored in SQLite with: `document_rkey`, `token`, `created_by_did`, `created_at` 59 + - Collaborator opens link → server validates token, adds their DID to `collaborators` array in document record via XRPC `app.bsky.feed.post` (actually custom `com.diffdown.document` PUT) 60 + - Collaborator authenticates via ATProto OAuth if not already logged in 61 + 62 + ## 4. WebSocket Protocol 63 + 64 + ### Message Types (JSON over WS) 65 + 66 + **Client → Server:** 67 + 68 + ```json 69 + { "type": "join", "rkey": "abc123", "did": "did:plc:..." } 70 + ``` 71 + 72 + ```json 73 + { "type": "edit", "delta": { "from": 10, "to": 15, "insert": "new text" } } 74 + ``` 75 + 76 + ```json 77 + { "type": "cursor", "position": 42, "selectionEnd": 55 } 78 + ``` 79 + 80 + ```json 81 + { "type": "comment", "paragraphId": "p-3", "text": "Nice paragraph!" } 82 + ``` 83 + 84 + **Server → Client:** 85 + 86 + ```json 87 + { "type": "presence", "users": [{ "did": "did:plc:...", "name": "Alice", "color": "#ff0000" }] } 88 + ``` 89 + 90 + ```json 91 + { "type": "edit", "delta": { "from": 10, "to": 15, "insert": "new text" }, "author": "did:plc:..." } 92 + ``` 93 + 94 + ```json 95 + { "type": "comment", "paragraphId": "p-3", "author": { "did": "...", "name": "Alice" }, "text": "Nice!" } 96 + ``` 97 + 98 + ```json 99 + { "type": "sync", "content": "full document text" } 100 + ``` 101 + 102 + ### Flow 103 + 104 + 1. Collaborator connects to `WS /ws/doc/{rkey}` with ATProto session 105 + 2. Server validates session, fetches document, checks DID is in `collaborators` list 106 + 3. Server adds to WebSocket room for that document 107 + 4. On edit: server broadcasts to all other clients in room 108 + 5. Server debounces (2s) and pushes to ATProto PDS 109 + 6. On disconnect: remove from presence list 110 + 111 + ## 5. Operational Transform 112 + 113 + Simplified OT approach: 114 + 115 + - Server maintains canonical document text 116 + - Edits are applied in order received 117 + - If concurrent edits conflict, last-write-wins with log (acceptable for MVP) 118 + - Full OT library (like ot-text) deferred to future 119 + 120 + ## 6. Authorization 121 + 122 + - ATProto session passed via WebSocket subprotocol or query param 123 + - Server uses existing `xrpcClient(r)` pattern for authenticated requests 124 + - Document access check: `collaborators` array must contain requester's DID 125 + 126 + ## 7. Backend Components (Go) 127 + 128 + ### New Package: `internal/collaboration/` 129 + 130 + | File | Purpose | 131 + |------|---------| 132 + | `hub.go` | WebSocket connection manager, room per document | 133 + | `client.go` | Represents one connected collaborator | 134 + | `invite.go` | Invite token generation (SHA256), validation | 135 + | `ot.go` | Operational transform helpers | 136 + 137 + ### Extended `internal/handler/` 138 + 139 + | Handler | Method | Purpose | 140 + |---------|--------|---------| 141 + | `DocumentInvite` | `POST /api/docs/{rkey}/invite` | Generate invite link | 142 + | `DocumentCollaborators` | `GET /api/docs/{rkey}/collaborators` | List current collaborators | 143 + | `CollaboratorWebSocket` | `GET /ws/doc/{rkey}` | WebSocket upgrade | 144 + | `CommentCreate` | `POST /api/docs/{rkey}/comments` | Create comment | 145 + | `CommentList` | `GET /api/docs/{rkey}/comments` | List comments for document | 146 + 147 + ### New Model Types 148 + 149 + ```go 150 + type Invite struct { 151 + ID string 152 + DocumentRKey string 153 + Token string 154 + CreatedBy string // DID 155 + CreatedAt time.Time 156 + } 157 + 158 + type Comment struct { 159 + URI string 160 + DocumentURI string 161 + ParagraphID string 162 + Text string 163 + AuthorDID string 164 + CreatedAt string 165 + } 166 + ``` 167 + 168 + ## 8. Frontend Changes 169 + 170 + ### `document_edit.html` 171 + 172 + 1. **WebSocket client** — connect on page load if collaborator 173 + 2. **Presence sidebar** — show active collaborators (name + color dot) 174 + 3. **Comment UI** — click paragraph → inline comment form 175 + 4. **Comment display** — show comment threads below paragraphs 176 + 5. **Visual feedback** — highlight own edits, indicate sync status 177 + 178 + ### CSS Updates 179 + 180 + - `.collaborator-cursor` — colored vertical bar with name label 181 + - `.comment-thread` — comment list below paragraph 182 + - `.presence-avatar` — small colored circle in sidebar 183 + 184 + ## 9. ATProto Integration 185 + 186 + ### Document Record Update 187 + 188 + When adding a collaborator, PUT to PDS: 189 + ``` 190 + POST /xrpc/com.atproto.repo.putRecord 191 + { 192 + "repo": creatorDID, 193 + "collection": "com.diffdown.document", 194 + "rkey": documentRKey, 195 + "record": { 196 + "$type": "com.diffdown.document", 197 + "title": "...", 198 + "content": { ... }, 199 + "textContent": "...", 200 + "collaborators": ["did:plc:existing", "did:plc:new"] 201 + } 202 + } 203 + ``` 204 + 205 + ### Comment Records 206 + 207 + Create comment: 208 + ``` 209 + POST /xrpc/com.atproto.repo.createRecord 210 + { 211 + "repo": commenterDID, 212 + "collection": "com.diffdown.comment", 213 + "record": { 214 + "$type": "com.diffdown.comment", 215 + "documentURI": "at://...", 216 + "paragraphId": "p-3", 217 + "text": "...", 218 + "author": commenterDID 219 + } 220 + } 221 + ``` 222 + 223 + ## 10. Edge Cases 224 + 225 + | Scenario | Handling | 226 + |----------|----------| 227 + | Creator deletes document | Close all WS connections, invalidate invites | 228 + | Collaborator loses access (removed) | WS disconnects, shows "Access removed" | 229 + | PDS unreachable | Show offline indicator, queue edits locally | 230 + | Concurrent edits conflict | Last-write-wins, log conflict for debugging | 231 + | Invite token expired | After 7 days, show "Invite expired" | 232 + 233 + ## 11. Limits 234 + 235 + - Maximum 5 collaborators per document (enforced on invite) 236 + - Maximum 100 comments per paragraph (soft limit) 237 + - WebSocket message size: 64KB max 238 + 239 + ## 12. Future Considerations 240 + 241 + - **Full CRDT**: Replace OT with Yjs or similar for better offline support 242 + - **Threaded comments**: Add `replyTo` field for nested replies 243 + - **Real-time cursor**: Show cursor positions and selections 244 + - **Inline suggestions**: Track suggestion mode (like Google Docs) 245 + - **Comment resolution**: Mark comments as resolved/dismissed