Skip to content

RFC: Hardening the SQLite persistence / Electric sync stack #1659

Description

@KyleAMathews

RFC: Hardening the SQLite persistence / Electric sync stack

Status: Active hardening plan
Audit baseline: 2026-09-16, main 3b99117
Scope: Correctness, durability, recovery, driver conformance, and type-composition fixes. Product-policy changes remain explicitly decision-gated.


1. Current starting point

The linked work was re-audited against current main. Resolved issues and superseded PRs have been removed from this RFC. The active inventory is now 15 open issues and 4 open PRs.

These reports are coupled. Most failures come from ownership or ordering mistakes between the sync source, persistence layer, collection lifecycle, coordinator, and SQLite driver. The dangerous terminal states are a collection that is ready but incomplete, a visible write that is not durable, or a startup that retries forever.

Original defect IDs are retained so existing discussion remains linkable; gaps are resolved items removed by this audit.

Area Defect Active issues / PRs Current-main assessment
Reset and resume consistency D1 #1589 Unresolved. A schema reset can wipe rows without clearing collection metadata, allowing a stale resume point to outlive its data.
Coordinator routing D2 #1589, #1753 Unresolved. Adapter ownership and schema-version routing are not consistently keyed per collection.
Cross-tab wire safety D3 #1498 Unresolved. Clone-unsafe request state can still cross BroadcastChannel.
op-sqlite result decoding D4 #1499 Unresolved. The v14 columnar executeAsync result can be interpreted as an empty SELECT result.
Offline readiness D6 #1416, #1443, PR #1615 Unresolved and policy-gated. Locally hydrated rows can remain blocked on an unavailable upstream.
Persistence durability D7 #1456, #1754, PR #1822 Partially improved, not resolved. Persistence failures remain insufficiently observable and a hydrate-window close can still strand a sync transaction. PR #1822 addresses only the stale replay-read slice.
Offline mutation persistence D8 #82, #865, #1456 Unresolved and policy-gated. Pending sync-present mutations are not a durable outbox.
SQLite expression indexes D9 PR #1487 Unresolved. Index DDL and runtime predicates use incompatible JSON-path SQL shapes.
Browser multi-tab topology D10 #1486, #1753 Unresolved and policy-gated. Web Locks leadership does not by itself provide safe ownership of an OPFS exclusive handle, and sync-ingested writes can bypass leadership.
Type composition D11 #1452, PR #1560 Unresolved. Persisted option results and vendor database types do not compose cleanly with createCollection.
Corruption recovery D12 #1567 Unresolved and policy-gated. A corrupt database can wedge startup indefinitely or produce an empty local-only collection.
Follower subset ensure D13 #1498 Unresolved. The leader can acknowledge a follower subset request without actually ensuring it.
Migration idempotence Additional active report #1711 Unresolved. Tauri startup can re-add replay_json and fail with a duplicate-column error.
Shared-driver scheduling Additional active report #1752 Unresolved. Cold-start persist work can starve hydration on the shared browser driver.

The earlier prepared commits and claim that the first three PRs could be split directly are no longer current: that branch has diverged substantially from main. Each fix must be rebuilt and proved against the audit baseline.

2. Release-gating invariants

  1. A destructive reset clears the rows, tombstones, applied transactions, and sync metadata it invalidates in one transaction.
  2. A sync adapter never trusts a resume point without a compatible, complete local baseline.
  3. Collection A's configuration or traffic cannot read, write, reset, or acknowledge work for collection B.
  4. Every coordinator message is clone-safe, routed to one collection, and reports success only after the requested work happened.
  5. A known driver result shape is decoded losslessly; an unknown shape is an error, never an empty result.
  6. Readiness has one explicit policy and settles exactly once across hydration, upstream readiness, failure, and cleanup.
  7. An accepted transaction becomes durable or replayable, or produces an observable failure. It cannot silently disappear on restart.
  8. Index DDL and indexed runtime predicates have the same SQLite expression shape.
  9. Exactly one supported owner accesses an exclusive OPFS database, including during leadership changes.
  10. Schema migrations are idempotent and corruption recovery cannot inherit stale sync state.

3. Oracle-first testing rule

All work under this RFC follows docs/contributing/oracle-tests.md. A PR starts with a reproducible RED oracle on current main and ends with that same oracle GREEN. A narrow witness is retained beside generalized histories; it is not replaced after the first fix.

Every oracle PR must name and demonstrate all six responsibilities:

  1. Contract / law and source: the invariant and the authoritative contract, issue, SQLite behavior, or explicit maintainer decision.
  2. Legal history / domain: valid event histories, including adversarial callback partitions and restart or cleanup boundaries.
  3. Independent oracle: a small reference model that does not call the production helper it judges.
  4. Production path / fixture: the public collection, adapter, driver, coordinator, or browser path exercised by the report.
  5. Exact observation / checkpoint: the rows, metadata, status, durable state, query plan, error, or schema inspected.
  6. Evidence: reach evidence that the intended branch ran; fault-control evidence that a known-bad implementation fails; deterministic replay data; and cleanup evidence proving late work cannot leak into the next test or lifecycle.

For unresolved product policy, models and tests may expose the alternatives but do not select the contract. The maintainer decision becomes the oracle's law before implementation begins.

4. Ordered implementation plan

Workstream 1 — reset and resume safety (D1, #1589)

RED oracle: Model a replica as rows, tombstones, applied transactions, metadata, and generation. Generate schema mismatch, partial restore, external row loss, restart, and empty up-to-date histories. Exercise persistedCollectionOptions plus the Electric stream fixture, and observe both SQLite state and the collection after restart. The preserved fault must leave a non-initial resume offset behind after clearing rows.

Fix and GREEN: Clear collection metadata in the same reset transaction as row state. Add a defensive resume validator that rejects a non-initial cursor when the persisted baseline cannot justify it, records a reset, and starts a clean snapshot. Prove atomic refetch, deterministic replay, and cleanup fencing.

Workstream 2 — per-collection coordinator correctness (D2, D3, D13; #1498, #1753, #1589)

RED oracle: Generate two or more collections with different policies and schema versions across leader/follower tabs. Partition and clone every coordinator message, request a follower subset, change leadership, and ingest sync writes. The reference model is a map keyed by collection identity; it permits no cross-key effects and acknowledges only completed requests. Observe rows, reset counts, message payloads, acknowledgements, and ownership.

Fix and GREEN: Replace mutable global adapter routing with a per-collection registry; define clone-safe wire DTOs; make follower subset ensure perform real leader work; and route all persistence writes, including sync-ingested writes, through the elected owner. Keep a fixed DataCloneError witness and a fixed cross-collection reset witness, then add browser multi-context histories.

Workstream 3 — op-sqlite and driver conformance (D4, #1499)

RED oracle: Define an independent statement-result decoder model for row-array, columnar rawRows plus columnNames, empty SELECT, and write-result shapes. Run SELECT-after-INSERT and pre-populated registry restart histories through the real driver contract fixture. An unknown result shape must be the fault control and must never be observed as an empty row set.

Fix and GREEN: Decode the op-sqlite v14 columnar shape before write markers, throw on unknown shapes, and run the expanded shared contract suite across every SQLite driver package.

Workstream 4 — transaction durability and hydrate-window races (D7, #1456, #1754, PR #1822)

RED oracle: Use an independent append-only transaction model and a deterministic scheduler to enumerate hydrate-window open/close, buffered sync commit, persistence failure, replay, cleanup, and restart interleavings. Observe the in-memory rows, durable rows, buffered transactions, error channel, and post-restart rows at exact checkpoints. Inject a known lost-flush mutant and a stale-replay mutant.

Fix and GREEN: Give every buffered transaction a terminal flush, replay, or visible-failure outcome; fence stale replay and hydration by lifecycle generation; and make persistence failures structured and observable. Evaluate PR #1822 inside this generalized suite—it may land as a slice, but it does not close #1754 until the lost-update history is green.

Workstream 5 — query planning and shared-driver scheduling (D9, PR #1487; #1752)

Expression-index RED oracle: Generate JSON paths and values independently, compare indexed query results with a scan model, and inspect EXPLAIN QUERY PLAN at the predicate checkpoint. Mutating either the DDL path or predicate path back to a bound parameter must fail. Rebase PR #1487 and merge only when the generalized oracle and package suite are green.

Scheduling RED oracle: Model the shared driver as ordered hydrate and persist work with explicit fairness bounds. Generate many cold-start collections and unrelated writes without using wall-clock timing as the verdict. Observe dequeue order, hydrate completion checkpoints, and bounded pending work; inject the current persist-first storm as the fault control.

Fix and GREEN: Make DDL and predicate expression shapes identical while continuing to bind values. Then add bounded batching or fair scheduling so persist work cannot starve hydration, and confirm the same order law through the browser/OPFS fixture.

Workstream 6 — decision-gated behavior

No implementation starts until each subsection records an explicit maintainer contract.

Readiness (D6, #1416, #1443, PR #1615): Decide whether an eager sync-present collection becomes ready after local hydration or remains upstream-gated. The oracle state machine must cover rows/no rows, offline upstream, on-demand mode, simultaneous hydration/upstream signals, failure, cleanup, and restart, observing one readiness transition and no late transition. Evaluate PR #1615 only after that law is recorded.

Offline outbox (D8, #82, #865, #1456): Decide whether unconfirmed sync-present mutations survive restart and how acknowledgement, rejection, and conflict affect them. If accepted, model persisted base plus pending delta independently and generate mutate/restart/ack/reject/conflict histories through the public mutation path. Observe base, outbox, visible rows, and durable rows. If deferred, document the non-durable contract explicitly rather than implying offline-write support.

Multi-tab topology (D10, #1486, #1753): Choose one owner model: a SharedWorker owns the OPFS handle, or followers route all adapter operations through the leader. The oracle must generate tab open/close, leader loss, delayed messages, and concurrent sync/local writes. A Playwright multi-context fixture observes handle owners, rows, resets, and eventual progress; a two-owner mutant must fail.

Corruption recovery (D12, #1567): Decide automatic quarantine/rebuild versus an explicit recovery option and define which collections are rebuildable replicas. After Workstream 1 is green, inject truncated, corrupt, and non-database files through real drivers. The independent model allows one close/quarantine/recreate attempt, preserves the corrupt artifact, rejects unrelated errors, and never inherits a stale resume point.

Workstream 7 — type and migration correctness (D11, #1452, PR #1560; #1711)

Type RED oracle: Compile a matrix of inferred and explicit schemas, spread-composed persisted options, valid/invalid collection configurations, and vendor SQLite database values. Independent type assertions state the expected inferred schema and rejected cases. Fix the option-result overloads and Expo vendor types, then merge PR #1560 or its rebased equivalent only when the matrix and package typecheck are green.

Migration RED oracle: Create real pre-upgrade Tauri databases at every supported schema version, including a database where replay_json already exists. Open, migrate, close, and reopen each twice. Compare PRAGMA schema information and preserved rows with a simple versioned-schema model; an unconditional duplicate ALTER is the fault control.

5. Landing and closure rules

  • Land workstreams in order unless a PR is demonstrably independent; Workstream 6 corruption depends on Workstream 1.
  • Each PR includes the RED commit or reproducible pre-fix result, the GREEN result, seed/replay instructions, and the focused package/type/lint commands.
  • Service-backed and browser-specific claims require their real fixtures; mocks alone do not close those issues.
  • Close an issue only when its fixed witness, generalized oracle, cleanup checks, and relevant full package suite are green on the final implementation.
  • Keep separate changesets and focused PRs where workstreams can ship independently.

6. Open maintainer decisions

  1. Eager sync-present readiness: local-first or upstream-gated.
  2. Offline outbox: durable pending mutations or an explicitly non-durable contract.
  3. Browser multi-tab ownership: SharedWorker owner or leader-RPC owner.
  4. Corruption handling: automatic quarantine for rebuildable replicas or an opt-in recovery API.

These decisions set observable contracts. Record each here before accepting its implementation.

7. Deferred architecture

A replica manifest, orthogonal storage/hydration/sync/durability status, generation-fenced reset operations, a database-level storage broker, authority classification, operational repair APIs, and a typed persistence-composition facade remain future directions. They are not prerequisites for the correctness work above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions