TinySync Understand Integrate Decide FAQ Playground ↗
Decide · Decision record

PowerSync as Sync Transport

EVALUATING2026-06-10 · prompted by a proposal to reuse Scribe's PowerSync stack for Drive

Could a managed row-sync service replace our custom metadata transport? Mostly yes — the question is whether we want it to.

Context

Drive's metadata transport today is custom. Postgres LISTEN/NOTIFY feeds an in-process WakeHub, which pushes WebSocket wake hints to connected devices; each client then replays GET /log?after=N against a client-owned cursor to pull the mutations it missed. It works. But it is unproven at scale and entirely ours to operate and harden — every reconnect storm, every dropped hint, every cursor edge case is on us.

PowerSync is a battle-tested managed service that syncs Postgres rows to client-side SQLite via change data capture (CDC). A collaborator proposed reusing Scribe's existing PowerSync implementation for Drive rather than continuing to build and harden our own pipe. This record evaluates that proposal. For how the current transport works end to end, see A File's Journey.

What it would replace — and what it wouldn't

LayerWith PowerSyncNotes
Downstream metadata transport Replaced CDC → PowerSync service → client SQLite; subsumes WakeHub, WebSocket hints, log replay, cursor/retry handling
Blob plane Unchanged Upload/download endpoints and blob-before-mutation ordering stay ours; ordering is enforced client-side either way
OS adapters Unchanged File Provider / CFAPI integration untouched — plus a small new state→event diff layer (see caveat b)
Server mutation handler Unchanged Validation, version checks, seq assignment stay ours; PowerSync's write path calls an endpoint we implement

The honest summary is that PowerSync cleanly replaces exactly one layer — the downstream metadata pipe — and that layer happens to be the one we have least confidence in at scale. The earlier objections ("but blobs!", "you still have to write the mutation handler") were true but beside the point: those layers are unchanged under either option. The decision is not "PowerSync or our whole architecture." It is "PowerSync or our custom transport for one well-bounded slice of the system."

The three caveats

(a) Auth impedance

PowerSync authenticates clients with JWTs. Our tsdev_ device tokens would need a JWT-issuing facade, and revocation becomes eventually-consistent: a revoked device keeps its sync stream until its JWT expires. For a product where "unlink stolen laptop" is a security feature, that lag must be a deliberate choice — short token TTLs, or explicit acceptance of the window — not an accident of the transport.

Revocation lag
Today, revocation cuts access on the device's next request — immediately. With PowerSync, revocation cuts access at JWT expiry: a revoked device keeps syncing until its current token runs out. The TTL you choose is the worst-case exposure window.

(b) State vs events

PowerSync delivers current row state; our OS adapters consume events (created, moved, deleted). A client-side diff layer that reconstructs the event from old row → new row is required to bridge them. The alternative — syncing the change_log table itself rather than the current-state rows — preserves event semantics directly, but fights PowerSync's bucket-compaction model with an append-only log it is not designed to carry. Diffing is the idiomatic path. It is modest but real work, and it lives entirely on the client.

(c) Operational dependency

PowerSync requires wal_level=logical and a replication slot on our Postgres, plus the PowerSync service itself — self-hosted OSS or paid cloud — sitting in the critical path of every device's sync. We are not removing an operational dependency; we are trading one for another. We give up "our untested WebSocket infrastructure" and take on "their tested infrastructure that we don't control." That is a reasonable trade, but it is a trade, and it should be named as one.

Verdict

Status: evaluating
PowerSync is viable as a downstream-only transport swap — this is not a technical incompatibility. It is contingent on the device/group identity model landing first (see Device Identity & Cardinality), because PowerSync's sync-rule bucket selection is driven by token claims: subject = device, claims = group memberships. Without a settled identity model, there are no stable claims to drive bucket selection. Next step: prototype the downstream-only path behind a flag before committing to it.

Addendum: evidence from Scribe's own design

Added after reviewing Scribe's published offline-sync design (client merge of y-indexeddb + PowerSync) — the system whose reuse prompted this record.

Scribe's design corroborates the scope map above, from the other side. In Scribe, PowerSync does exactly one job: background fan-out of an append-only op log (scribe_doc_updates) to every team member's local SQLite. Everything else lives elsewhere — merge semantics are Yjs CRDT, upstream writes go through Hocuspocus only (their own stated invariant: editing clients "never INSERT to PowerSync's upload queue"), and replicated rows sit inert in SQLite until a doc-open replays them into the editor's Y.Doc. PowerSync is their downstream pipe, not their sync engine. That is precisely the downstream-only hybrid this record recommends prototyping.

Their field experience also refines two of the caveats:

One framing correction the comparison forced: the difference between the two products is not eager vs lazy sync — both sync their tree eagerly and defer content to the consumption point. The real asymmetries are that Scribe's content (small Yjs text diffs) rides inside the replicated rows, so PowerSync delivers tree and content in one channel, while Drive's content is unbounded binary that cannot — leaving PowerSync covering only the metadata slice; and that applying tree changes is a declarative UI re-query for Scribe but imperative File Provider / CFAPI calls for Drive. The full comparison is Scribe vs Drive.

Consequences

If adopted — we delete the WakeHub and WebSocket wake path entirely, gain battle-tested sync infrastructure, and accept two new costs: the vendor dependency in the sync critical path, and the JWT-issuing facade in front of our device tokens (with the revocation lag it implies).

If declined — we keep full ownership of the protocol end to end, and we own the obligation to harden and load-test the custom transport ourselves before it meets real scale.

Either way, the blob plane, the OS adapters, and the server mutation logic are unaffected; the choice is scoped to the downstream metadata pipe alone. This record exists because the proposal deserved a real evaluation, not a reflexive "our way is fine."