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
| Layer | With PowerSync | Notes |
|---|---|---|
| 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.
(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
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:
- Caveat (a) is softer than stated. Scribe propagates access loss data-style: when a user loses access, PowerSync delivers a
REMOVEfor the affected rows and the client clears its local cache. Membership flows through sync-rule data rather than JWT claims; the JWT is just session auth. The same pattern would carry Drive's group membership, shrinking — though not eliminating — the revocation-lag window. - Caveat (b)'s trade-off is field-tested. Scribe syncs the log itself (append-only rows) and pays the predicted tax: a scheduled compaction job once a doc exceeds 20 rows, plus defensive cursor handling for clients whose cursor points at a compacted-away row. Viable, but real machinery — a concrete cost to weigh against the sync-current-state + diff-layer path.
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."