# Collaboration: live co-editing and shared notes Extends `specs/mobile-and-sync/spec.md`, which listed both as non-goals. The owner asked on 2026-10-20 for two devices to work on the same note at the same time, and for notes to be shared between different users. ## Problem File sync converges edits after the fact: two people typing in one note produce a merge or a conflict copy, and a vault has exactly one owner. There is no way to give someone else a folder of notes or to see who is editing now. ## Goal Two or more people (or two devices of one person) can edit the same note at the same time, see each other's cursors, and have every keystroke converge in under a second, inside a vault or folder the owner shared with chosen roles. ## Non-Goals - Rich-text or block CRDTs; the document is the note's plain Markdown text. - Live editing inside end-to-end encrypted vaults (the server must read operations to order them; E2EE vaults keep file sync only, stated in the UI). - Public web pages, anonymous link editing, comments and suggestions (P2, later), organisation or team billing. ## Design decisions 1. **Live model: server-ordered operational transformation (OT) on plain text.** Reasons: one central server already exists; the note stays a normal `.md` file; no new third-party dependency (a CRDT crate such as `yrs`, `automerge`, `loro` or `diamond-types` would be a dependency needing owner approval and a binary document format beside the `.md`). A CRDT's advantage (offline merge) is already covered by file sync + `merge3`. Ceiling: OT with one server needs the server to be online for the live part; offline editing falls back to file sync. 2. **Pure crate `notescraft-collab`** (L1): `TextOp` (components Retain/Insert/Delete over Unicode scalar counts), `apply`, `compose`, `transform`, `invert`, cursor/selection transform, the OT client state machine (synchronised / awaiting ack / awaiting with buffer), the server `Room` (document text, revision, bounded op history, join snapshot, `submit(client_id, base_rev, op) -> Accepted{rev, op}`), presence records. Convergence is property-tested (`apply(apply(s,a),b') == apply(apply(s,b),a')`). 3. **Transport without new dependencies**: `POST .../live/{path}/join` returns `{text, rev, peers}`; `POST .../live/{path}/ops` carries `{client, base_rev, op, cursor}`; `GET .../live/{path}/events` is a Server-Sent Events stream of `op`, `ack`, `presence`, `leave`. (axum SSE; the client reads the stream line by line on a worker thread with `ureq`.) 4. **Persistence**: a room writes its text to the vault ledger as a normal write (`PUT` semantics, author = the last editor) after 2 s without edits and when the last peer leaves; file-sync clients receive it like any change. A client that holds a live session ignores the echo when its hash already matches. 5. **Accounts and roles**: users (`id`, display name, colour) own devices; a vault has members with a role `owner`, `editor` or `viewer`. Viewers read; editors read, write, join rooms; owners also manage members and invites. Every endpoint checks the role server-side; the audit log records invites, joins, role changes, removals. 6. **Sharing unit = a shared vault mounted as a folder.** The owner marks a folder (or creates a new shared vault); recipients mount it at `Shared//` in their own vault with its own sync state (`.notescraft/sync/.json`). Reuses the whole sync client; no per-file ACLs in v1. 7. **Invites**: owner creates a single-use code with a role (valid 48 h, hashed in storage, lockout like pairing); the recipient enters the code or opens a link `notescraft://join?server=...&code=...`; accepting adds a membership and the mount. 8. **Presence**: colour, name, cursor and selection of every peer, ephemeral (never stored), pruned 30 s after the last heartbeat. ## Many vaults (owner requirement, 2026-10-20) One person has many vaults (work, personal, a shared project, a course) and belongs to vaults other people own. Rules: - **Account-level device token.** A device belongs to a user, not to one vault; its token works for every vault the user is a member of (role per vault). Pairing a device once connects the whole account. (The current server binds a token to one vault; the accounts task migrates it: existing devices become members of a personal user.) - **Server API**: `GET /v1/vaults` (vaults I belong to: id, name, role, members count, bytes used, quota, head seq, last change), `POST /v1/vaults {name}` (creator is owner), `PATCH /v1/vaults/{v} {name}`, `DELETE /v1/vaults/{v}` (owner; soft delete with a 14-day grace, audited). Limits: 50 vaults per user, name 1..80 chars. - **Client binding per local vault**: `.notescraft/sync/binding.json` = `{server, vault_id, user_id, device_id}`; the token lives in the OS keychain under `notescraft-sync//` (one entry per account, not per vault). A local vault with no binding is simply not synced. Two local vaults may bind to two different server vaults, even on two different servers. - **Vault picker UI**: a Vaults screen lists local vaults and server vaults I can still connect, each with a sync pill, role badge and members; actions: Open, Connect to a server vault, Create server vault from this local vault, Leave. Desktop gets the same list in the vault switcher popover. - **Scale**: the server loads ledgers lazily and keeps at most 64 in memory (LRU, reloaded from SQLite); rooms and SSE streams are per (vault, path); audit, quota and GC are per vault; no endpoint ever lists another user's vaults. - **Background work**: only the open vault syncs with a live loop; other bound vaults sync on open, on a 15-minute timer on desktop, and by the OS background task on mobile (battery and rate friendly). ## User stories - As an owner, I want to share a folder with a colleague as editor, so that we keep project notes together. - As an editor, I want to see my colleague's cursor and edits appear as they type, so that we do not overwrite each other. - As a viewer, I want read-only access, so that I cannot break the owner's notes. - As an owner, I want to see members, change roles and remove someone, so that access stays under my control. - As anyone, I want an edit made offline to merge when I reconnect, so that live mode never loses work. ## Acceptance Criteria - [ ] Given two clients in one room, when both type at the same time (property test with random interleavings), then after all messages are delivered both documents are identical and no insert is lost. - [ ] Given a client typing during a 300 ms network delay, then its own edits show at once and the remote edits transform around them (cursor stays on the same word). - [ ] Given a viewer token, when it calls `PUT`, `DELETE`, `join` for editing or `ops`, then the server answers 403 and the audit log records nothing secret. - [ ] Given an invite code used twice or after 48 h, then the second use answers 401; 5 failures lock invites for 10 minutes. - [ ] Given the last peer leaves, then within 2 s the note file on the server equals the room text and other devices receive it through file sync. - [ ] Given a removed member, then their token gets 403 on that vault immediately and their local mount keeps its files but stops syncing ("removed from shared folder"). - [ ] Given a user with 12 vaults on one server, when a device pairs once, then `GET /v1/vaults` lists exactly those 12 with roles and no other user's vault, and one token reads and writes each according to the role. - [ ] Given two local vaults bound to different server vaults, when both are opened in turn, then each syncs only with its own server vault and its own `.notescraft/sync/` state. - [ ] Given more than 64 vaults active over time, then the server stays under its ledger cache cap and every vault still answers correctly after eviction. - [ ] Given an E2EE vault, then the editor offers no live session and says why. - [ ] Given the server restarts mid-session, then clients rejoin, resend their unacknowledged op once, and end identical. ## Constraints - Stack: Rust only; egui UI; server in `apps/notescraft-sync-server`. - Performance: op round trip under 200 ms on LAN; room history capped (1000 ops, then snapshot); room memory capped per document (note size cap 2 MiB for live mode). - Security: roles enforced on the server; presence names are display text only (sanitised, max 40 chars, no markup); no tokens or codes in logs or audit. - Compat: plain `.md`; Obsidian-compatible vault unchanged. ## Open Questions 1. Owner approval for a CRDT crate instead of in-house OT? Recommendation: no, build OT first (no dependency, matches plain files). 2. Per-folder sharing inside one vault (instead of mounting a separate shared vault)? Recommendation: v2. 3. Public read-only links and comments: later; ranked in `research/`.