Sync Core Concepts

Oversqlite manages sync source identity internally. App code owns authentication and account attachment, but it does not generate, persist, or rotate sourceId.

Three Different Identities

Sync Writer Identity

Oversqlite maintains one current sourceId for the local runtime.

Important properties:

  • it is the sync writer identity used on the wire
  • it is opaque and debug-only from app code
  • it lives in oversqlite metadata inside the local database
  • it may rotate during explicit recovery on the same install
  • it also rotates after successful destructive detach() / syncThenDetach()

If your app/backend also has a deviceId, treat that as a separate concept. A product-level deviceId may remain stable while oversqlite rotates the current sync writer identity.

Attached Account Identity

This is the authenticated userId passed to attach(userId).

Account attachment decides which remote account scope is currently active for the local database.

Auth Identity

This is whatever your backend uses in tokens or sessions.

Oversqlite does not own auth. The server authenticates the request separately, and oversqlite sends the current sync writer identity as sync transport metadata.

Lifecycle Model

The oversqlite lifecycle is:

  1. open()
  2. attach(userId) whenever an authenticated session exists
  3. normal sync operations
  4. detach() or syncThenDetach() when leaving the attached account

open()

open() is local-only.

It validates managed-table configuration, creates or repairs lifecycle metadata, installs local triggers, restores or creates the current internal sourceId, and captures pre-existing managed rows once when bootstrap policy allows it.

It never talks to the server and never attaches an account.

For KMP JS/Wasm browsers, the generated client uses the database’s ordinary packaged worker and deterministic direct-OPFS target; it does not inject a snapshot backend. A one-time byte-level legacy import, when needed, completes before generated schema migrations and before this Oversqlite lifecycle opens its ten control tables. Retained legacy bytes and migration/health markers are local storage evidence, not a replacement source identity or a remote rebuild. JS Node uses the same default provider with transient in-memory storage.

attach(userId)

attach(userId) is the authenticated lifecycle step. It may:

  • resume the same attached account
  • use authoritative remote state
  • authorize a first local seed upload
  • start an authoritative empty scope
  • return RetryLater

Call it whenever an authenticated session exists, not only on the first sign-in gesture.

For a new or remote attachment, the client first checks /sync/capabilities and rejects a different advertised sync-table contract before /sync/connect or snapshot work. A same-user durable resume is deliberately network-free; its first later remote operation performs the same check.

Advertised Sync-Table Contract

Every capabilities response must include registered_table_specs. SQLiteNow treats it as the canonical server table contract and compares it with the already-validated local configuration:

  • table order does not matter
  • logical schema, managed table names, and ordered sync-key columns must match exactly
  • missing, null, blank, duplicate, or malformed metadata is an invalid capabilities response
  • a valid difference throws SyncTableContractMismatchException with sorted server-only, client-only, and sync-key mismatch diagnostics

This is fail-fast table/key compatibility checking. It does not negotiate projections, payload columns, or a wire profile. Snapshot table checks remain in place as defense in depth.

Automatic Downloads

Automatic downloads are an optional attached-session worker. They are not part of open() or attach(userId) and must be started explicitly by app code.

The worker only downloads. It may poll pullToStable() or, when the server supports features.bundle_change_watch, use /sync/watch as a metadata-only wake-up stream. The watch payload is never applied directly; pullToStable() remains the authoritative remote-data path.

detach()

detach() safely removes the current attached account scope from the local database.

It is fail-closed: if attached pending sync data still exists, it returns DetachOutcome.BLOCKED_UNSYNCED_DATA and makes no destructive local changes.

If destructive cleanup succeeds, detach() clears managed local state and immediately rebinds the anonymous database to a fresh internally generated sourceId.

If detach is blocked, rolls back, or only cancels a pending remote_replace, the existing source id is preserved.

syncThenDetach()

syncThenDetach() is bounded convenience sugar. It runs sync() and then attempts detach(). If new local writes arrive during the previous round, it may retry a small number of times. It never loops forever, and it returns the final blocked outcome explicitly if detach still cannot proceed. When the final detach succeeds destructively, it rotates to a fresh internal source exactly like plain detach().

Authority States

For the currently attached account, oversqlite reports:

  • PENDING_LOCAL_SEED
  • AUTHORITATIVE_EMPTY
  • AUTHORITATIVE_MATERIALIZED

These are scope/materialization states, not authentication states.

Rebuild And Recovery

rebuild() is the optional explicit recovery entry point. Pull-side history_pruned and checkpoint_ahead conditions durably set the rebuild gate before snapshot work begins, and normal sync() or pullToStable() automatically resumes checkpoint recovery after interruption.

Important rules:

  • it remains an attached/authenticated operation
  • it rebuilds local managed tables from the authoritative remote snapshot
  • oversqlite chooses the internal mode
  • app code does not supply a replacement sourceId

In ordinary checkpoint-recovery cases, recovery keeps the current source. Pending offline work is frozen and uploaded first when safe; an unresolved upload returns a typed actionable blocker while preserving the app row, outbox, checkpoint, and rebuild gate. The checkpoint advances only in the final atomic snapshot apply.

In source-recovery-required cases, rebuild() preserves frozen unsynced intent, rebuilds from the snapshot, rotates to a fresh internally generated source, and restores the frozen intent under that fresh source stream.

Diagnostics

sourceInfo() exposes read-only source diagnostics.

Use it for logging, support tooling, or debug UI only.

Important rules:

  • SourceInfo.currentSourceId is opaque
  • callers must not persist it externally
  • callers must not infer lifecycle meaning from its format
  • callers must not treat it as a control surface

What open() Does Not Mean

open() does not mean:

  • the account is attached
  • the server session is connected
  • the local DB was synchronized
  • remote state was rebuilt

Even when durable local attachment metadata already exists, you should still call attach(userId) when an authenticated session is present so the client can resume connected sync operations.