TinySync Understand Integrate Decide FAQ Playground ↗
Decide · Analysis

Scribe vs Drive: One Pattern, Two Surfaces

ANALYSIS2026-06-10 · sourced from Scribe's published offline-sync design

Two products in the same ecosystem converged on the same sync skeleton — and the places where the analogy ends are exactly the places where "just reuse it" breaks down.

Context

Scribe is the ecosystem's collaborative-documents product — Notion/Confluence-shaped: rich-text docs, live multi-user editing, team workspaces. Drive is the file product — Dropbox/Google Drive-shaped: a vault that appears as a native folder in Finder or Explorer. Both need offline-capable sync, and both teams designed their sync layers independently.

Scribe published its offline-sync design (client merge of y-indexeddb + PowerSync), and a proposal followed: Drive should reuse Scribe's stack. This page compares the two architectures honestly — what genuinely converged, what genuinely differs, and what that means for reuse. The transport-specific verdict lives in PowerSync as Sync Transport; this page is the architectural comparison behind it.

The two stacks, side by side
flowchart LR subgraph S["Scribe — collaborative docs"] direction TB SE["Editor"] SY["Y.Doc — CRDT merge"] SH["Hocuspocus — live WebSocket"] SP[("PowerSync local SQLite")] SL[("scribe_doc_updates - append-only log")] SE --> SY SY <--> SH SL -- "PowerSync replication" --> SP SP -- "replay on doc-open" --> SY SH -- "checkpoint diffs" --> SL end subgraph D["Drive — file sync"] direction TB DF["Finder / Explorer"] DA["OS adapter — File Provider / CFAPI"] DE["Sync engine — version checks, conflict copies"] DS[("local SQLite mirror")] DL[("change_log - append-only log")] DF <--> DA DA <--> DE DE <--> DS DL -- "log replay via cursor" --> DE DE -- "mutations via POST" --> DL end

The convergent core

Neither team copied the other, yet both arrived at the same skeleton: an append-only operation log with a monotonic sequence number, a client-owned cursor, catch-up by replay, a live channel layered on top for connected clients, and a compaction story to bound log growth. This is the "dumb server, smart cursor" pattern — the server is a log plus an auth check; all sync-progress state lives at the edge (see Device Identity & Cardinality).

ConceptScribeDrive
Append-only log scribe_doc_updates (seq BIGSERIAL) change_log (vault_id, seq)
Client-owned cursor lastSeqApplied in y-indexeddb after=N cursor in engine SQLite
Catch-up Phase-2 replay: WHERE seq > lastSeqApplied GET /log?after=N replay
Live channel Hocuspocus WebSocket — carries the ops themselves WebSocket wake hints — deliberately carry nothing
Log growth control Compaction job (collapse old rows past 20 per doc) Snapshot + min_retained_seq retention
Single writer to the log scribe-backend — clients never insert Sync server — clients never insert

One instructive divergence inside the convergence: Scribe's live channel carries data and doubles as the upstream write path, while Drive's wake hints are read-only and content-free — "a hint says something happened, never what" — with upstream writes on a separate mutation POST. Same role in the architecture, different trust placed in the channel.

Where PowerSync actually sits in Scribe

The reuse proposal rests on "Scribe uses PowerSync." So look at what PowerSync does — and does not do — in Scribe's own design:

ConcernHandled byPowerSync's role
Merge / conflict semantics Yjs CRDT none
Upstream writes Hocuspocus only — their own invariant: editing clients "never INSERT to PowerSync's upload queue" none
Live propagation Hocuspocus WebSocket none
Turning rows into something the user sees Phase-2 replay into the Y.Doc → editor none — rows sit inert in SQLite until the doc opens
Background fan-out of the op log to team SQLite PowerSync this, and only this

In the flagship PowerSync deployment, PowerSync is a downstream-only delivery pipe for an append-only log — the actual sync engine lives elsewhere. Transposed to Drive, the faithful equivalent is the downstream-only hybrid evaluated in the PowerSync record: PowerSync ferries metadata rows to client SQLite while the mutation path, conflict logic, blob plane, and OS adapters remain untouched. Scribe's design is evidence for that scoped swap — and evidence against "reuse Scribe's implementation," because the parts Scribe delegates to PowerSync are the parts Drive already has, and the parts Scribe keeps custom (merge, upstream, presentation) are exactly Drive's hard parts.

Same tool, opposite justification
Scribe needed PowerSync because Yjs ops have no queryable rest state — without row replication, a closed doc's content exists nowhere on the client. Drive's metadata already has a rest state: the engine's SQLite mirror, maintained by log replay. Scribe used PowerSync to gain a capability it lacked; Drive would use it to replace a capability it has. That asymmetry is what the "just reuse it" argument glosses over.

Where the products genuinely differ

Merge semantics: text merges, files don't

A Scribe doc is a Yjs CRDT — concurrent edits merge operation-by-operation, and two people typing in the same paragraph both keep their words. That works because rich text has a merge function. A file does not: it is opaque bytes, and there is no principled way to merge two concurrent saves of report.pdf. Drive's honest answer is optimistic concurrency — version-checked mutations, with the losing write preserved as a conflict copy (see Truth & Conflict). Neither approach transfers to the other product: CRDT merge is meaningless for opaque blobs, and conflict copies would be a regression for collaborative text.

Content transport: in-band rows vs a blob plane

A Scribe doc's entire content is the sum of its Yjs diffs — small base64 strings that ride inside the replicated rows. Once PowerSync has delivered the rows, the full doc is reconstructable offline; content and metadata share one channel, and full offline availability of every doc is a free side effect of the transport. Drive's content is unbounded binary. It cannot ride rows, so it lives in a content-addressed blob plane with its own ordering invariant (blob-before-mutation) and its own availability policy — placeholders by default, hydration on open, pinning for offline (see A File's Journey). Any row-sync transport covers all of Scribe's data problem but only the metadata slice of Drive's.

Presentation surface: your own UI vs the operating system

When tree rows land in Scribe's SQLite, "applying" them is a declarative re-query — the app re-renders its own list view, which row-sync tooling supports natively with watched queries. When tree rows land in Drive's SQLite, "applying" them means making Finder agree: imperatively creating and removing placeholders and signalling enumerators through File Provider and CFAPI, continuously, as rows arrive. There is no "re-render the OS from a query." This is the layer that makes Drive hard, and no part of Scribe's stack addresses it (see Meeting the OS).

Not eager vs lazy
Both products sync their tree eagerly in the background and defer content to the consumption point — Scribe replays diffs into the editor on doc-open, Drive hydrates bytes on file-open. The asymmetry is not when they sync; it is what the channel can carry, and who owns the surface the result is painted on.

Identity and access: where the device token went

Scribe has no device tokens in its sync layer — and at first glance that looks like a simplification Drive should copy. It isn't, but seeing why takes the comparison apart into authentication and authorization.

Scribe's model

Authentication: identity is the user, established by the ordinary web login session. PowerSync requires a JWT, so Scribe's backend mints short-lived JWTs from that session whenever the client needs one. No device registration, no device rows, no long-lived sync credential.

Authorization: sync rules assign rows to buckets, and which buckets a client receives is computed from the JWT's user identity joined against membership data. When a user loses access, the membership row changes, PowerSync recomputes her buckets, her clients receive REMOVE operations, and the frontend clears the local cache. Her JWT never changes; nobody rotates a credential. Access is a property of data, and access change is more data flowing down the same pipe.

Why Scribe can skip device identity — and Drive can't

The device identity record gives four reasons device-level identity is necessary. Scribe escapes each one, for reasons that don't transfer:

NeedHow Scribe escapes itTransfers to Drive?
Idempotency of writes Clients never write to the synced table (one-way writes; upstream is Hocuspocus), and Yjs ops are idempotent by construction — applying an op twice is a no-op No — Drive clients issue mutations that must dedupe server-side
Echo suppression Built into the CRDT: every Y.Doc carries a clientId, and the (clientId, clock) pair makes re-applying your own ops harmless No — file operations are not self-deduplicating
Revocation granularity The browser session is the per-device credential; "sign out that laptop" is handled by the web auth stack, below the sync layer No — Drive's daemon has no browser session layer; it authenticates headless at boot
Attribution created_by at user level suffices for docs Mostly — file sync wants device-level attribution ("which machine uploaded the corrupt file")

Note the detail hiding in the second row: Yjs has device identity inside it. The clientId in every state vector — {A: 6, B: 6} in Scribe's own worked example — is a per-device-session identifier. Scribe didn't eliminate device identity; the CRDT machinery absorbed it, unauthenticated, which is safe only because CRDT merge makes spoofed or duplicated ops harmless to correctness. Drive has no such absorbing layer.

The comparison with Drive's group model

On the authorization side the two models are the same shape. Drive's vault access is a membership edge (device → group → vault) resolved at request time: adding or removing access touches zero tokens. That is the same access-as-data property Scribe gets from sync rules — with one difference in Drive's favor: per-request resolution is immediate, where bucket recomputation is pipeline-mediated.

The only real difference is the authentication subject, and it is forced by platform, not design taste. Scribe's client lives in a browser, which provides per-device sessions and can re-mint short-lived JWTs whenever online — the sync layer gets device-scoped credentials for free and never has to name them. Drive's client is a headless daemon that must authenticate at boot, offline-tolerant, with no interactive login. Something must be the long-lived credential, and that something is the device token. The device token isn't extra machinery on top of a session; it is the session, for a client that has no browser.

Does Scribe's model simplify fanout?

Credential fanout: no. Scribe holds n sessions for n browsers (managed by the auth stack); Drive's target model holds n tokens for n devices. Both linear. The thing that was actually quadratic — Drive's legacy per-vault credential rows — is fixed by the group model, and Scribe's approach would not improve on n + m + membership edges.

Data fanout: Scribe's is heavier, not lighter. Their design states it plainly: every team member's SQLite receives every scribe_doc_updates row, whether or not they ever open the doc. Full content, pushed to everyone, always — affordable when content is tiny text diffs, and exactly the move Drive cannot make with blobs. Drive's hint-plus-pull model exists precisely to avoid pushing everything to everyone.

The transferable half
"No device tokens" is a benefit of running inside a platform that already has a session layer — the browser. A daemon must mint its own session, and that is all a device token is. The idea that does transfer is the other half: access as data (membership edges, REMOVE propagation), which Drive's group model already implements — arguably more directly, since it resolves per request instead of per pipeline.

What transfers — and what doesn't

Transfers:

Does not transfer:

Bottom line
Same skeleton, different organs. Both products converged on log + cursor + replay because that is what offline-first sync wants. The reuse question is really a layer question: the transport layer may transfer — the engine and presentation layers cannot. "Scribe uses PowerSync" is true, and in Scribe's own design PowerSync never touches the three things that make Drive hard: merge semantics, upstream writes, and the OS surface.