Many people, one document, no conflicts. A real-time collaborative editor built on CRDTs — including a CRDT written from scratch.
Birga is a collaborative editor where several people edit the same document at once — live cursors, instant updates, and edits that merge without conflicts, even offline. Two people typing in the same place never clobber each other; a client that reconnects after an hour catches up cleanly.
The interesting part is not the editor UI — it's the synchronisation. Birga contains a small CRDT (Conflict-free Replicated Data Type) implemented from scratch for sequences (text), so the merge logic is understood and owned, not imported. On top of that, a production-grade editor uses a battle-tested CRDT library, so the app is real, not a demo.
Real-time collaboration is where "I know Socket.io" ends and "I understand distributed state" begins. CRDTs are the hard, senior part: convergence, causal ordering, tombstones, offline reconciliation. Building one from scratch — and being able to explain why it converges — is a signal almost no junior portfolio carries.
- Collaborative editing of rich text — multiple users, one doc, live.
- Presence / awareness — live cursors, selections, who's here.
- Offline-first — edit offline, reconnect, converge automatically.
- Persistence — documents survive server restarts; late joiners load fast (snapshots).
- From-scratch CRDT — a documented sequence CRDT (RGA/Logoot-style) with a test suite proving convergence, used to teach the concept; the full editor runs on a mature CRDT for reliability.
TypeScript · a hand-written CRDT core (own package) · WebSocket sync server (Node, ws) · Next.js +
TipTap editor · Yjs for the rich-text path · Redis for presence/fan-out · Postgres for snapshots.
graph LR
A[browser A] -- ops + awareness --> S[["@birga/server<br/>WebSocket relay"]]
B[browser B] -- ops + awareness --> S
S --- PG[(Postgres<br/>snapshots + op log)]
S --- R[(Redis<br/>pub/sub + presence)]
Each browser runs an editor bound to a CRDT — the from-scratch @birga/crdt
for plain-text mode, Yjs for rich text — persists locally to IndexedDB
(offline-first), and syncs ops through a CRDT-agnostic relay that stores them and
fans out across instances.
pnpm install
pnpm build && pnpm test # 264 tests green (incl. the CRDT property suite)
# run the app (builds libs, then server + web in parallel)
pnpm dev # web on :3000, sync server on :8080, API on :8787Open http://localhost:3000, sign up (or continue as a guest), create a document, open the same URL in a second window, and type in both — including with one window offline. They converge.
No database is required to run it. Documents, accounts, presence and the op log live in memory and the server says so at boot; everything works — sign-up, sharing, permissions, both editing modes — it simply does not survive a restart.
Durable / multi-instance:
pnpm db:up # Postgres + Redis via docker compose
DATABASE_URL=postgres://birga:birga@localhost:5432/birga \
REDIS_URL=redis://localhost:6379 \
SESSION_SECRET=$(openssl rand -base64 32) \
pnpm dev:server # persistence, shared rooms, TTL'd presencePermission enforcement on the WebSocket relay is on by default; set
ENFORCE_PERMISSIONS=0 only if you deliberately want an open relay.
SESSION_SECRET is required in production, and random-per-process otherwise.
Seeded demo (a populated, role-based tour):
pnpm db:up
export DATABASE_URL=postgres://birga:birga@localhost:5432/birga
export SESSION_SECRET=demo-secret
pnpm seed # 5 documents, 3 personas, plain + rich content
DATABASE_URL=$DATABASE_URL SESSION_SECRET=$SESSION_SECRET \
DEMO_ACCOUNTS=1 pnpm dev:server
pnpm dev:webOn the sign-in screen, click Ada, Ben, or Carol — each sees the document list with their own owner/editor/viewer roles, real content in both editing modes, and (as a viewer) a live read-only editor that rolls the edit back and says so.
Every phase of the spec's MVP scope is implemented and tested — the full
pipeline (build · typecheck · lint · test · web build) passes, and CI
(ci.yml) runs all of it on every push. Full technical
spec: docs/TZ.md.
| package | what it is | tests |
|---|---|---|
@birga/crdt |
from-scratch RGA sequence CRDT + convergence property suite | 25 |
@birga/protocol |
CRDT-agnostic wire protocol | — |
@birga/server |
WS relay · Redis fan-out + TTL'd presence · Postgres persistence + auto-compaction (both surfaces) · REST API (accounts, docs, share links, revocation) · access control on by default | 138 |
@birga/client |
offline-first sync engine (CRDT ⇆ protocol) · reconnect · rollback | 18 |
@birga/web |
Next.js editor — plain (@birga/crdt) + rich (Yjs) · presence · doc list · sharing |
83 |
How it maps to the spec's Definition of Done (§9): the CRDT converges under thousands of randomised interleavings (a green property suite); two-window live edit and offline-edit-then-reconnect converge; live cursors + presence; late joiners load from a snapshot on long docs (periodic compaction); and the README explains why the CRDT converges with a diagram — see the portfolio write-up, docs/CRDT.md.
Beyond the spec: real email/password accounts alongside guest identities, revocable sharing, live "N here now" on the document list, and an app-wide dark theme. Remaining polish: a richer rich-text schema, document export, and search.
MIT.