a YC-backed documentation platform

Git-style branching, for people who have never used git

A version-control system for rich structured documents: lazy branches, an identity-based three-way merge, and conflicts that get resolved inside the editor — never in raw markers.

Role: design & implementation, full stack · Surface: merge engine, API, editor & review UI · Stack: TypeScript · Node.js · React

The brief: prepare big changes in private, publish them in one reviewed step

Documentation teams kept asking for the workflow engineers take for granted: stage a large change — a release's worth of docs, a restructure, a rewrite — without touching what customers currently read, get it reviewed, then publish it atomically. That's a branch, a pull request, and a merge.

The problem is that every part of git's answer assumes things that aren't true here. The content isn't text files — it's a tree of rich structured documents. The users aren't engineers — they will never resolve a conflict in raw markers. And the "repository" is a live collaborative workspace that keeps changing while the branch is open. The brief was to deliver git's guarantees without any of git's interface.

One branch, end to end

PUBLISHED ──●──────●──────────────────●─────────────────────────●──▶
                   │ branch created    ┆ upstream keeps moving  ╱ merge & publish
                   │ (0 docs copied)   ┆                       ╱  (atomic, reviewed)
                   ╰──●─────────●──────▼────────●─────────●───╯
BRANCH              first      edits  pull    resolve    review
                    edit              upstream  + commit
                    (forks a          (three-way (in the
                    shadow copy)      merge)     editor)

The full lifecycle. Four branch states — open, in review, merged, archived — and every step reversible until publish.

Why git's playbook doesn't transfer

Content model — documents aren't lines

Line-based diff and merge — git's whole foundation — is meaningless for a tree of rich-text blocks: tabbed panes, tables, API references, embeds. The merge unit had to be rethought from scratch.

Audience — writers, not engineers

No CLI, no staging area, no <<<<<<< markers. Every concept — branch, commit, pull, conflict, merge — had to surface as something a writer already understands, inside the editor they already use.

Concurrency — upstream never stops

While a branch is open, the published workspace keeps evolving under real-time collaborative editing. Branches must pull those changes in — repeatedly — and merge cleanly back over whatever happened since.

Scale — branching must feel free

Workspaces hold hundreds or thousands of documents. If creating a branch meant copying them, nobody would branch. Cost had to be proportional to what actually changes, not to workspace size.

Decision 01 — Branches that copy nothing

Creating a branch copies zero documents. The branch gets its own document tree — a lightweight index — whose entries still point at the published documents, plus a pinned snapshot version marking the branch point. Only when someone first edits a document does the system fork a shadow copy from that snapshot and swap the tree entry to point at it.

Copy-on-write makes branching instant regardless of workspace size, and it makes the review surface honest: the set of shadow copies is the set of changed documents. Storage, diffing, and merging all scale with the edit, not the workspace.

Decision 02 — Merge by identity, not position

The merge engine works at the level documents are actually made of: blocks. Every block carries a stable ID that survives copying, so when a branch merges, each block is matched across the three versions — the common base, the published side, and the branch side — by identity, and classified by what happened to it on each side:

Published sideBranch sideMerge result
untoucheduntouchedkeep as-is
editeduntouchedtake published edit
untouchededitedtake branch edit
editedsame editkeep once
editededited differentlyconflict marker
deletededitedconflict marker
deleteduntoucheddeletion applies
added on either sidewoven in near authored position

The block-level decision matrix. A two-pass algorithm walks one side in order, then weaves the other side's additions in next to their original neighbors.

This engine is itself a rewrite — and the reasons are documented at the top of the file. The first version matched blocks by array index, which fell apart the moment either side inserted, deleted, or moved a block:

// Why this exists: the previous merge matched changes by array *index*.
// The moment either side inserted, deleted, or moved a block:
//   - bogus conflicts: two unrelated additions at the same index were paired;
//   - smeared edits:   "modify index N" landed on whatever block sat at N;
//   - duplicated content: one side's shifted tail survived verbatim.
// It also silently *missed* genuine conflicts when the same block sat at
// different indices on each side.

From the merge engine's header comment — the failure taxonomy that drove the redesign.

One more subtlety: inline review comments are branch-scoped, so equality is always computed on comment-stripped clones. An annotation can never masquerade as a content change and trigger a false conflict.

Decision 03 — Conflicts are editor blocks, not markers

When both sides changed the same block differently, the merge doesn't fail and doesn't dump marker text into the document. It inserts a conflict block — a first-class editor element that wraps both versions, fully rendered, with a one-click resolution:

Conflicting change — choose a version

Published versionUse this version Rate limits apply per workspace: 60 120 requests per minute across all API keys.

This branchUse this version Rate limits apply per workspace API key: 60 requests per minute**, with burst allowances on paid plans**.

Word-level differences are computed with an LCS diff and highlighted inside the rendered content.

The same block type does double duty for informational diffs — "here's what changed upstream, no action needed" — flagged so they never block anything and dissolve automatically at the next commit. Only genuine unresolved conflicts gate publishing, and the writer resolves them where they resolve everything else: in the editor.

Decision 04 — Pull is staged; commit makes it real

Pulling upstream changes into a branch is deliberately a two-phase operation. The pull three-way-merges every affected document and stages the result as pending state — pinned version markers on the branch and on each affected tree entry. Nothing is baseline until the writer commits.

Commit is where the guarantees concentrate. It refuses to proceed while any structural conflict or unresolved conflict block remains — naming the document that's blocking — auto-resolves the informational ones, promotes the pending merge state into the branch's baseline, and records a commit with the full tree snapshot. Every branch starts with a control commit pinning its starting point, so there is always a well-defined diff base and "no changes yet" is detectable, not assumed.

Every destructive step has an inverse: reset a document to its last commit, discard the whole branch, or restore — publishing is the only one-way door, and it's gated by review.

Decision 05 — A diff that reads like the editor

Review lives or dies on the diff view, and naively diffing structured blocks produces noise: an entire tabbed component flagged because one label changed, or a "change" that's really just which tab happened to be selected. The client-side diff builder picks the smallest presentation that can show each change:

  • Volatile view state is invisible. Selected-tab indices and similar UI ephemera are stripped before comparison — they can never register as edits.
  • Containers recurse. If a tabbed block's own settings are unchanged, it renders once and the diff descends into the one tab that actually changed, marking only that.
  • Config-heavy blocks get field-level diffs. For blocks that are mostly data — API references, code panels — the diff lists humanized field changes ("Endpoint URL changed") instead of showing two giant near-identical blocks.
  • Prose gets word-level highlights, and some block types render a single merged copy with insertions and deletions painted directly inside the live block UI.

The result is a review surface where a ten-word edit in a large document reads as a ten-word edit — not as two full-page copies the reviewer must eyeball.

Decision 06 — The branch became the platform's universal primitive

Once branch + merge + review existed as reliable machinery, other features collapsed into it:

  • Reader suggestions become branches. Public readers propose edits; each confirmed suggestion materializes as a real branch with a real commit. Accepting one runs the full three-way merge — a suggestion that conflicts with current content, or that a reviewer has since edited, is refused with a reason rather than blindly overwriting the page.
  • Reverts become branches. Restoring an old revision never overwrites in place. From the published view it creates a ready-to-review branch containing the revert; inside a branch it commits the revert through the same guarded commit path as any other change.
  • Mode switches convert in both directions. Workspaces can turn the branching workflow on or off at any time; pending suggestions convert to branches on enable and back to lightweight suggestions on disable, idempotently — concurrent switches race-safely claim each item with a conditional update, and the loser rolls back its work instead of minting a duplicate.

Correctness in the seams

  • Preflight before side effects. Content snapshots live in object storage, which doesn't roll back with a database transaction — so flows that write-then-commit run every refusal check before touching content, ensuring a rejection can't leave a phantom revision behind.
  • Comment isolation on merge. Branch-scoped review comments are stripped as content merges back; upstream comments survive only where the text they anchor to survived.
  • Dual-identity lookups. A branched document answers to two IDs — its shadow ID and the original — and every lookup in the system matches on either, so no flow breaks depending on when a document was forked.
  • Self-healing baselines. Bulk-imported documents that never got a version snapshot are versioned on the fly at first branch-edit instead of failing the fork.
  • Dangling links fail loudly. A suggestion pointing at a branch that no longer exists is an error with a message — never a silent fallback that overwrites the published page with a stale snapshot.

By the numbers

0documents copied at branch creation
8outcomes in the block-merge matrix
15API endpoints
4branch lifecycle states
2passes in the merge algorithm
3features rebuilt on the branch primitive
~2klines dedicated to diff rendering alone
~12.5klines of TypeScript, engine to editor

The shipped system gives documentation teams the engineering workflow — isolate, iterate, pull upstream, review, publish atomically — with none of its interface. Writers branch without knowing what copy-on-write is, merge without knowing what a three-way merge is, and resolve conflicts by clicking on the version they want, inside rendered content. The merge engine is trusted enough that three other features route their writes through it rather than maintaining their own paths to published content.


TypeScript · Node.js / Express · React · PostgreSQL · S3-backed content revisions · block-based rich-text editor · real-time sync. Client and product details anonymized. Architecture, algorithms, and figures are drawn from the shipped implementation. Companion piece: An AI agent that rewrites your documentation — the agent stages every write on this branching system.