genuinebasil.dev/Projects/Marginal
DepthArchitectureCommsPhasesStackDecisions
LIVE · 6 SERVICES
Rust/CRDTs/event sourcing

No merge dialog.
Ever.

A self-hosted, real-time collaborative markdown notebook. Several people on one page at once with nothing to resolve by hand, IDE-grade diagnostics on prose, per-actor undo that survives interleaved edits, and history you can scrub — because the operation log is the truth.

Go to marginal The decisions

Status: running in production. Six Rust services behind a gateway, four Postgres instances, NATS and Redis — thirteen containers, live at marginal.genuinebasil.dev. The repository also holds the architecture, the data model, eleven ADRs, and the roadmap for the Rust port now under way.

THE OP LOG IS THE TRUTH3 ACTORS
the log is the source of truth
block rows are a projection
undo is a new op, not a rollback|
ana
sam
4029ins“projection” @ 128
4030ret64
4031del7
ENGINEERING DEPTH the ●●● phases

The roadmap marks each phase by Rust density. These are the ●●● ones — where the work is genuinely hard and no crate is doing it for me.

CRDTPHASE 3
ins del converged — no dialog

Concurrent edits from several people converge on one document with nothing to resolve. Written from scratch — importing it would leave a notebook app with nothing learned in it.

RopePHASE 3
24

A balanced tree over text, so splicing into the middle of a long document is O(log n) rather than a full copy. Compiled to wasm and shared with the browser.

Write-ahead logPHASE 3
appended acked

Ops are batched lock-free off the socket thread and appended to a WAL before broadcast, so a crash loses nothing that was already acknowledged to a client.

Incremental analysisPHASE 4

A symbol table plus an engine that re-analyses only what changed. The block tree is the analyser's cheapest advantage — an edit's blast radius is a subtree, not the whole file.

Fact dependency graphPHASE 4
invalidate

A build system for prose: each diagnostic is a derived fact with declared inputs, so invalidation propagates exactly as far as it must and no further.

Per-actor undoPHASE 5
youthem undo skips theirs

Undo that touches only your own edits across a log interleaved with everyone else's — tractable only because undo is a new op computed against the log, never a rollback of shared state.

Tree diffPHASE 6
moved, not del+ins

Structural diff over the block tree, so a moved section reads as a move rather than a delete plus an insert — the version-history algorithm a line-based Myers diff cannot do.

Inverted indexPHASE 7
docs1 2 3 4 “rope” → 1,3 “crdt” → 2,4 “wal” → 3

Tantivy in-process plus the backlink graph, and two deliberately different fuzzy searches — one for a page you half-remember, one for autocompleting a link mid-keystroke.

Consistent hashingPHASE 10
vnodes~1/N moves

Session routing on a virtual-node ring with a failure detector and rehash, so every client on a page reaches the same stateful instance even as the fleet changes under them.

SYSTEM ARCHITECTURE — 11 SERVICES
react 19 SPA + rust wasm editor core api-gateway RS256 verify · rate limit · circuit breaker · WS routing documentpages · blocks · treeoutbox · LTREE collaborationCRDT · rope · WAL[stateful] diagnosticsanalyzers · symbol table[degradable] historyop replay · snapshots[cold path · CQRS] searchtantivy · backlinksHNSW (ph. 19) auth notification publishing plugin assistant NATS JetStream event bus — the outbox's subscribers · postgres 18 · redis · minio
SERVICE COMMUNICATION gRPC is the east-west default — ADR-007
FROM → TO TRANSPORT SHAPE WHY THIS AND NOT THE OTHER
browser ↔ collaborationWebSocketbidiedits and cursors flow both ways continuously; nothing else fits
gateway → collaborationWS + hash routestickyevery client on a page must reach the same stateful instance
collaboration → documentNATS JetStreamasync eventthe projection may lag; the editor may never wait on it
collaboration → diagnosticsNATS JetStreamfire & forgetdegradable by design — no squiggles beats no typing
document → searchgRPCclient streambulk reindex is a stream of many small writes, not one big call
history → documentgRPCunarycold path — latency here costs nothing anybody feels
gateway → authlocal verifyno callthe RS256 public key is cached; auth is never on the hot path
SIX TRACKS · 11 OF 22 PHASES SHIPPED ●●● deep rust · ●●○ real but familiar · ○○○ no rust

Ticked phases are running in production today. Order is a dependency question, so it lives in the roadmap; when is an estimate and does not. Track 1 deliberately ends on the deepest phase — shipping the MVP early costs no depth. Cloud is not a track at the end: a phase is not done until its increment is deployed.

01
MVP
log in, write a page, edit it live with someone else
✓ SHIPPED
✓ 1 — Documents ●●○
Pages, blocks, tree, outbox, block-granular ops, a single-user editor that saves
✓ 2 — Auth ●●○
Argon2id, RS256, refresh rotation, login
✓ 3 — Collaboration ●●●
Rope, CRDT, WAL, live cursors — the demo
02
The differentiators
the three things it does that nothing else does
✓ SHIPPED
✓ 4 — Diagnostics ●●●
Analyzers, symbol table, incremental engine, squiggles
✓ 5 — Undo / Redo ●●○
Per-actor undo across collaborative edits
✓ 6 — History ●●○
Event-sourced replay, snapshots, scrubber (CQRS)
03
Distributed systems
where six services stop being decoration
● IN PROGRESS
✓ 7 — Search ●●●
Tantivy, inverted index, link autocomplete
✓ 8 — Page-delete saga ●●○
Choreographed saga, compensation, idempotency
✓ 9 — API gateway ●●○
Tower stack, rate limiting, circuit breaker
10 — Session routing ●●○
Consistent hashing, failure detector, rehash
04
Knowledge platform
ADR-009 · ordered by dependency, not by theme
QUEUED
13 — Identity & RBAC
Users, roles, LTREE permission inheritance
14 — Comments ●●●
Anchored threads, mentions, PN-Counter reactions
✓ 15 — Notifications
The outbox's first real subscriber
16 — The full editor ●●●
All block directives, tables, footnotes, slash menu, ⌘K
20 — Settings & admin
Feature flags, theme families, instance settings
05
Reach & extensibility
the parts that make it a platform rather than an app
QUEUED
17 — Publishing
Public pages via static pre-render, feeds, newsletter
18 — Plugins ●●●
wasmtime sandbox, fuel metering, capability scoping
19 — Assistant ●●●
AI that emits ops, HNSW vector index
21 — Related content ●●●
SimHash + LSH, PageRank, bridge suggestions
06
Cloud hardening
Google Cloud is the primary target — ADR-008
● IN PROGRESS
✓ 11 — Containers & CI ○○○
Multi-stage builds, cargo-chef, keyless Actions via Workload Identity Federation
12 — Observability ○○○
Cloud Trace, Managed Prometheus, Grafana, SLOs, a DR drill, a cost review
STACK AT A GLANCE
Axum + Tower + Tokio
HTTP and the middleware stack
PostgreSQL 18 + sqlx
JSONB, LTREE, uuidv7(), recursive CTEs
tonic + prost
gRPC, all four RPC modes — ADR-007
NATS JetStream
the event bus behind the outbox
Redis
cache and presence
Tantivy
in-process full-text search
Rust → wasm32
editor core: model, rope, marks, selection, ops
React 19 + Vite
SPA, Tailwind v4, Radix UI — ADR-004
utoipa → OpenAPI
contract generated, then typed for the client
Terraform → GCP
the primary deployment target — ADR-008
OpenTelemetry
Jaeger / Cloud Trace, Prometheus, Grafana
MinIO / Cloud Storage
object storage, local and cloud
DECISIONS WORTH DISCUSSING
CRDT

Written from scratch rather than pulled in as a crate. The whole objective is Rust depth — importing the interesting part would leave a notebook app with nothing learned in it. The tradeoff is real: a mature CRDT library is better tested than mine will be for a long time.

event sourcing

The op log is the source of truth and block rows are a projection — not an audit table bolted beside a mutable one. This is what makes per-actor undo tractable: undo is a new op computed against the log, never a rollback of shared state.

scope

ADR-001 draws the boundary hard: no databases, no formulas, no workspaces, no permissions matrix. Three things done unusually well, chosen so the interesting engineering cannot be dodged by adding features instead of depth.

phase 0

Foundation work is pulled by the first service that needs it, never pushed ahead of one. Building shared crates before a consumer exists is the speculative abstraction the project structure explicitly forbids — extract on the third use.

cloud

Cloud is not a track at the end. A phase is not done until its increment is deployed — Phase 1 already carries Terraform, Cloud SQL, Secret Manager and a first deploy. Cloud Run first, GKE arriving at Phase 3 when a stateful service finally needs it.

design

There is no red anywhere in the interface. A notebook has no compile step, so nothing in it is broken — the most severe state available is amber, and it reads as advisory rather than as an error.

live →Decisions© 2026 genuinebasil.dev│
← blogcairn →