Convex replication module for PowerSync.
replication:
connections:
- type: convex
deployment_url: https://<your-deployment>.convex.cloud
deploy_key: <your-deploy-key>
polling_interval_ms: 1000
request_timeout_ms: 30000- Simplest is to run the convex demo in the self-host-demo repo
The content below is written in an agents.md style describing the behavior of module-convex.
- This module replicates Convex data into PowerSync bucket storage.
- Source APIs used are Convex Streaming Export: (
json_schemas,list_snapshot,document_deltas). - Initial scope is default Convex component only, but we could consider support for custom components in the future if we can figure out consistency.
- Deploy keys grant root access (read/write on all tables), components could address this later.
- Initial replication:
- Initial replication pins a global Convex snapshot boundary using
list_snapshot. If this is omitted, it provides the global snapshot boundary ref. - Snapshot each selected Sync Streams table with that fixed
snapshot. - First per-table snapshot call omits
cursor; pagination cursor is only for later pages in the same run. - Commit snapshot LSN, then switch to deltas.
- Initial replication pins a global Convex snapshot boundary using
- Streaming replication:
- Start from persisted resume LSN.
- Poll
document_deltasusing frequency configured inpolling_interval_ms - Always stream globally (no
tableNamefilter), then filter locally by selected Sync Streams tables. - If a table is first seen in a
document_deltaspage and matches Sync Streams, snapshot it inline at that page boundary and skip that table's delta rows from the same page, because the snapshot already includes them.
snapshotis the consistency boundary; pagecursoris pagination state.- All table snapshots in a run must use the same pinned
snapshot; if response snapshot differs, fail fast. - On restart during initial replication:
- Reuse persisted snapshot LSN boundary.
- Resume table page walk from the persisted per-table
lastKeycursor when available. - If the last page was already flushed before interruption, mark the table snapshot done without re-reading rows.
- Delta streaming starts from resume LSN (snapshot boundary), not from table page cursor.
tablePattern.connectionTagand schema must match before table selection.- Source table replica identity is
_id. - The overall system must ensure causal consistency of replicated data in bucket storage.
- Convex snapshot and delta cursors are always
i64timestamps (serialized as decimal numeric strings in JSON). - The
list_snapshotpagination cursor is a separate JSON-serialized{table, id}string — it is pagination state, not a replication cursor. - Persisted Convex LSNs must be canonical 19-digit numeric cursor strings.
ZERO_LSN = "0"remains the internal sentinel.
- Auth header:
Authorization: Convex <deploy_key>. - Always request
format=json. - Parse large numeric JSON using
JSONBig. - Retry classification:
- retryable: network, timeout, 429, 5xx.
- non-retryable: malformed responses, auth/config issues.
- Convex
json_schemasdoes not provide a schema change token or revision cursor that can be checkpointed. - Current behavior uses
json_schemasfor discovery/debug, but does not continuously diff source schema versions. - Operational caveat: if Convex schema changes (tables or columns), developers must review and redeploy Sync Streams manually.
- Future improvement: cache a canonicalized
json_schemashash, poll periodically, and raise diagnostics when schema drift is detected.
- Current runtime mapping in stream writer:
| Convex Type | TS/JS Type | SQLite type |
|---|---|---|
| Id | string | text |
| Null | null | null |
| Int64 | bigint | integer |
| Float64 | number | real |
| Boolean | boolean | Up to developer - string or number |
| String | string | text |
| Bytes | ArrayBuffer | text |
| Array | Array | text |
| Object | Object | text |
| Record | Record | text |
- Convex does not expose a native
Datewire type; timestamps arrive asnumberorstring. - BLOB values are valid row values but are not valid bucket parameter values.
createReplicationHeadmust:- resolve global head cursor,
- write a Convex checkpoint marker via
POST /api/mutation(callspowersync_checkpoints:createCheckpoint), - then pass the head to callback.
- Source marker table:
powersync_checkpoints- Convex rejects table names starting with
_, so no leading-underscore variant is used. - The table has a single
last_updatedfield; the mutation upserts one row (bounded to one row total). - The developer must deploy the
powersync_checkpointsschema and mutation to their Convex project.
- Convex rejects table names starting with
- Stream handling requirement:
- checkpoint marker tables must always be excluded from replicated source tables and ignored in delta row application.
- marker-only delta pages must trigger immediate
keepalivecheckpoint advancement (do not wait for 60s throttle).
-
The default schema is
convex -
On an idle system, multiple successive calls to
/api/document_deltaswill return the same cursor value i.e. the cursor is not wall clock based. -
Mutation Transaction Atomicity in
document_deltas- The
cursorin/api/document_deltasis a Convex commit timestamp (i64), not a per-operation counter. - Every Convex mutation is an ACID transaction that commits with a single timestamp; all writes within that mutation share the same
_tsvalue in the delta stream. - Therefore, the cursor advances once per mutation, not once per individual CRUD operation inside it.
- Example: a mutation that deletes 5 documents and updates 3 produces 8 entries in
document_deltas, all with identical_ts. - The Convex backend enforces this by never splitting a page mid-timestamp: when the row limit is reached mid-transaction, the page extends until all rows at that
_tsare included before stopping. - Consequence for replication: all writes from a single mutation always appear in the same
document_deltaspage and are committed to bucket storage atomically as one batch.
- The