Collaboration app architecture
Collaboration app architecture: the decisions that matter
The architecture of a collaboration app comes down to 4 choices: CRDT (Yjs) update log instead of operational transform, SSE + POST over Postgres in phase 1, websocket sync tier in phase 2, append-only update log with periodic snapshots and compaction and hard limits on document size, room size and update rate, failing closed. Each is explained below with the alternatives that were rejected and what you accept by choosing it.
Last checked
CRDT (Yjs) update log instead of operational transform
Situation: Editors must type concurrently, work offline and converge without a central transformer that becomes a single point of failure and a correctness trap.
Choice: Clients hold a Yjs CRDT; each local change is a small binary update that commutes and is idempotent. The server persists updates as an ordered, append-only log and relays them; it never transforms operations.
- Rejected: Operational transform (Google Docs style): needs a central server to order and transform every op, much harder to get right for rich structures and offline, but yields compact ops.
- Rejected: Last-write-wins on whole documents or fields: trivial, silently loses concurrent edits.
- Rejected: Locking / single editor at a time: safe but not collaboration.
- Consequence: Convergence is a library guarantee, not custom server code.
- Consequence: Documents carry CRDT metadata overhead (typically 1.5-3x plain size) and need periodic compaction/garbage collection.
- Consequence: The wire format is transport-agnostic, so moving from SSE+POST to websockets is a transport swap.
SSE + POST over Postgres in phase 1, websocket sync tier in phase 2
Situation: Locked stack has no websocket tier or broker; most collaborative rooms are small and tolerate a few hundred milliseconds of latency.
Choice: Writes are idempotent HTTP POST batches into doc_updates; reads are SSE streams woken by LISTEN/NOTIFY and fed by tail reads on (doc_id, seq). A websocket tier is a documented phase-2 upgrade with a measurable trigger.
- Rejected: Websocket server from day one: lower latency but adds a stateful tier, sticky routing and failure handling before there is demand.
- Rejected: Third-party realtime SaaS: fast to start, but unbounded per-message pricing and a data-processor dependency.
- Rejected: Polling only: simplest but p95 latency and database load are poor.
- Consequence: Delivery is at-least-once and idempotent; reconnect uses the last seen seq.
- Consequence: Per-node stream count is the scaling limit, so it is capped and monitored.
- Consequence: Awareness (cursors) on SSE is best-effort and may lag the websocket tier.
Append-only update log with periodic snapshots and compaction
Situation: Storing only the latest state loses history and offline merge; storing every update forever makes cold load and storage grow without bound.
Choice: Persist every accepted update in an append-only log with a per-document seq, cut a snapshot every N updates / bytes / idle time, delete covered updates after retention, and make named snapshots the version history.
- Rejected: Rewrite the full document on each save: simple, but write amplification and lost concurrent edits.
- Rejected: Keep every update forever: perfect history, unbounded cold-load time and storage cost.
- Consequence: Cold load is one snapshot plus a bounded tail.
- Consequence: Restore is a new update, so history stays append-only and auditable.
- Consequence: Compaction is a background job with its own queue and alerts.
Hard limits on document size, room size and update rate, failing closed
Situation: One pathological document, a script spamming updates, or a viral link can degrade every other tenant and run up unbounded storage and egress cost.
Choice: Every dimension (update bytes, document bytes, updates per minute, connections per document/user/node, storage, stream-hours) has a code-enforced limit; hitting it refuses the operation with a 4xx/429 and a metric.
- Rejected: Soft limits with alerts only: an email is not a cap.
- Rejected: Autoscale to absorb load: cost becomes unbounded and violates the zero-surprise rule.
- Consequence: Users occasionally see an explicit limit message instead of a slow product.
- Consequence: Limits are plan attributes, so they double as the pricing lever.
How the data is indexed
Indexes are chosen from the questions the app asks most often. Each line below names a table and the question its main index answers.
- documents: (workspace_id, updated_at DESC) — workspace file list, cursor paginated.
- doc_updates: unique (doc_id, seq) — total order per document and the tail read for fan-out.
- doc_snapshots: (doc_id, up_to_seq DESC) — cold load takes the latest snapshot then the tail.
- doc_members: unique (doc_id, user_id) — one role per person per document, idempotent invites.
- share_links: unique (token_hash) — O(1) lookup by hash of the link token.
- comments: (doc_id, thread_id, created_at) — render one thread in order.
- presence_sessions: unique (doc_id, session_id) — one row per browser tab, upsert on heartbeat.
- collab_usage: unique (workspace_id, day) — one row per workspace per day, upsert in batches.
What Forge64 provides underneath
Forge64 matches your description to one of 12 reference architectures, writes the plan and data model into the project, generates the app, and runs a quality gate over the code before you see it. Each app gets its own Postgres database and sign-in with no per-user fee. Hosting on Forge Cloud and Forge Payments are rolling out.
This is a reference design for the category, not a promise that every item is generated today. Built-ins such as email, background jobs, file uploads and search are coming soon, so anything here that needs them is something you add yourself for now.
FAQ
- What is the most important architecture decision for a collaboration app?
- CRDT (Yjs) update log instead of operational transform. Clients hold a Yjs CRDT; each local change is a small binary update that commutes and is idempotent. The server persists updates as an ordered, append-only log and relays them; it never transforms operations.
- Do I have to make these decisions myself?
- No. Forge AI plans them from your description and writes the decisions into the project as architecture records you can read and change.
- What do the alternatives cost me?
- For example, for "SSE + POST over Postgres in phase 1, websocket sync tier in phase 2": Websocket server from day one: lower latency but adds a stateful tier, sticky routing and failure handling before there is demand. Third-party realtime SaaS: fast to start, but unbounded per-message pricing and a data-processor dependency.
Build your app with Forge64
One prompt to a running app with auth, database, hosting and payments. Start free, no credit card required.
Start building for free