Advanced Concepts
This page describes the supported bundle-based sync model.
Authoritative state
Registered business tables are the source of truth. Sync metadata is derived from committed business-table effects rather than projected afterward from a separate authoritative log.
Bundles
A bundle is the smallest durable sync unit.
- One committed business-table transaction becomes one committed sync bundle.
sync.bundle_logrecords committed bundle metadata.sync.bundle_rowsrecords normalized row effects for that bundle.- Clients apply complete bundles only.
Structured keys
Rows are identified by structured sync keys, not by an implicit single pk field on the wire.
In the current supported envelope, each registered table still exposes exactly one visible sync
key column, but that column may be uuid or text. Visible sync key values are strings on the
wire, and UUID-valued keys must use canonical dashed lowercase UUID text.
Row state
sync.row_state stores the authoritative per-row replicated state for conflict detection,
idempotent replay, and snapshot rebuilds.
row_versionis the authoritative version seen by clients.deleteddistinguishes live rows from tombstoned rows.- the key is
(user_id, schema_name, table_name, key_json)
Timestamp encoding
For syncable business rows, timestamp columns should be stored as SQLite TEXT using UTC
RFC3339 or RFC3339Nano values. This keeps payloads portable across SQLite, JSON transport, and Go
resolver code.
- use parsed time comparison in custom merge logic such as
updated_atconflict policies - do not treat legacy formats such as
yyyy:mm:dd hh:mm:ssas the canonical synced form
Push
Push is all-or-nothing at bundle level.
- The client sends one logical dirty set with
source_id,source_bundle_id, andbase_bundle_seq. - The server validates the whole request and either rejects it or commits one bundle.
- Retrying the same accepted
(user_id, source_id, source_bundle_id)returns the same committed bundle.
Structured conflict recovery
The supported client/runtime contract includes structured push_conflict recovery.
- Only decoded machine-readable
push_conflictpayloads participate in resolver-based recovery - Valid resolver outcomes are:
- accept server state
- keep local intent
- keep merged full-row payload
- Automatic structured recovery rewrites local row state, requeues surviving dirty intents,
clears
_sync_outbox_*, and retries from a fresh outbound snapshot - Structured retries preserve the same logical
source_bundle_id;next_source_bundle_idadvances only after a successful committed replay - The retry budget is bounded to
2automatic retries inside onePushPending()
Pull
Pull is frozen to a stable ceiling.
- The first
GET /sync/pullresponse returnsstable_bundle_seq. - Follow-up requests must keep using that ceiling until it is reached.
- Clients advance
last_bundle_seq_seenonly after durable local bundle apply.
Snapshot rebuilds
Chunked snapshot sessions return the full current after-image at one exact frozen bundle sequence.
Use it for:
- first hydration
- destructive recovery
- rebuild after
history_pruned
When rebuild requires source rotation:
- the client reserves one durable
replacement_source_idlocally and reuses it across restart or retry - the rotated
POST /sync/snapshot-sessionsrequest tells the server to retire the old source and reserve the replacement source atomically - the client keeps the old local
current_source_iduntil authoritative snapshot apply succeeds - normal source-sequenced sync stays fail-closed while recovery is pending
Fail-closed contract
The supported envelope is intentionally strict.
- bootstrap fails for unsupported FK/key shapes
- bootstrap fails when the managed or registered table set is not FK-closed
- bootstrap fails when required FK deferrability is missing
- pull/hydrate/recover fail while local dirty rows exist
- malformed server responses are rejected without advancing durable checkpoints
- invalid structured conflict resolutions clear
_sync_outbox_*and restore replayable intents to_sync_dirty_rows - structured conflict retry exhaustion also clears
_sync_outbox_*and leaves unresolved intents replayable - generic non-conflict commit/replay failures still use the existing fail-closed recovery path
Supported envelope
The current contract is designed to be reliable for:
- exactly one visible sync key column per registered/managed table
- scope-bound registered PostgreSQL identity with
_sync_scope_id - scope-inclusive FKs between registered PostgreSQL tables
- self-references
- multi-table cycles
ON DELETE CASCADEON UPDATE CASCADE
Unsupported shapes must fail at bootstrap rather than degrade into partial runtime behavior.