Relationships
#3507 Datastore rework Phase 4: a provider-supplied replica target (replicaTarget()) so v3 replicates to the datastore's own bucket, plus conformance
Opened by stack72 · 10/15/2026· Shipped 10/15/2026
Background (read this first)
swamp is reworking its datastore layer (tracking: swamp-club#2865). In the new format, v3, every change is a commit in a single-writer SQLite commits.db. Content is stored as content-addressed chunks, and each checkout rebuilds its files from the commits. v3 is opt-in today for filesystem datastores (swamp-club#3279), and swamp-uat's filesystem-v3 backend tests it in CI.
Phase 4 is "solo mode on buckets":
- the CLI is the single writer of a local
commits.db; - Litestream replicates it to the bucket;
- chunks go to the bucket through
storage/1; - a fenced writer lease makes sure only one machine writes.
Shipped so far:
- A, swamp-club#3357: replication cost, choosing an idle sidecar.
- B, swamp-club#3358: the pinned, hash-checked Litestream binary.
- C, swamp-club#3359: the fenced writer lease (
src/domain/datastore_v3/writer_lease.ts,src/infrastructure/datastore_v3/v3_writer_lock.ts). Every CLI write command takes a new fence, serve keeps one for its lifetime, andcommits.dbkeeps a store-widewriter_fencethat refuses lower fences. - D1, swamp-club#3416: a dormant Litestream sidecar (
src/infrastructure/datastore_v3/litestream/,src/domain/datastore_v3/litestream_replication.tsandreplication_lineage.ts).- A write command confirms its commits are replicated (
POST /syncwithwait,replicated_txid >= txid, then the lease check) before it succeeds. - Replication follows a lineage: the replica path is
_litestream/<fence>/, named by the fence at which this copy's replication began. Another writer's fence restarts it on a new path. The lineage is recorded inlineage.jsonbesidecommits.db, with abandoned ones inpreviousLineages. - Nothing replicates unless the internal
SWAMP_V3_REPLICAsetting (replica_setting.ts, tests only) names a file or S3 replica.
- A write command confirms its commits are replicated (
- D2, swamp-club#3482: restore, refresh and takeover, also dormant.
- While replicating, each checkout keeps its own
commits.dbunder.swamp/_datastore_v3_copies/. - Lineages are published under
_control/lineage/<fence>/(started.json,head-<writer fence>.json,confirmed.json). - A restore truncates to the published head's high-water mark.
- See "Phase 4D2" in
datastore-litestream.md.
- While replicating, each checkout keeps its own
The decisions are in design/enablers/datastore-litestream.md ("Decision", "Restore, failover and fences (Q6)", "Phase 4D1") and in design/enablers/datastores.md (the "v3 …" sections).
- F, swamp-club#3490: the commit outbox. D3, swamp-club#3489: serve confirms each write. swamp-club#3491: D2's follow-ups.
- The extensions,
@swamp/s3-datastoreand@swamp/gcs-datastore2026.10.15.2 (swamp-club#3395, #3492):- a writable datastore-wide control plane (
datastoreControlPlaneStore(),datastoreWideLease) that keeps_control/writer/,_control/lineage/and_control/datastore-formatat the root; storage/1chunk storage.
- a writable datastore-wide control plane (
Goal
Let the datastore itself tell swamp where Litestream replicates, so Phase 4E can replace the test-only SWAMP_V3_REPLICA setting with the datastore's own bucket.
Today the replica comes from SWAMP_V3_REPLICA (src/infrastructure/datastore_v3/litestream/replica_setting.ts: {type: "file", path} or {type: "s3", bucket, path, endpoint?, region?}). Only the extension knows how its config maps to a bucket location, so core must not parse a provider's config shape.
Design
A new optional provider method in the datastore provider contract (
packages/testing/datastore_types.ts, and core's mirror of it):replicaTarget(): V3ReplicaTarget. It describes where this datastore's Litestream replica lives:- S3 or S3-compatible:
{ type: "s3", bucket, prefix, region?, endpoint?, forcePathStyle? }; - GCS:
{ type: "gcs", bucket, prefix }.
prefixis the datastore root's key prefix, ending without a slash or empty. Core appends_litestream/<fence>/(replicaPathFor), so the provider never builds replica paths itself.- S3 or S3-compatible:
No secrets in the descriptor. Both first-party extensions resolve credentials from the default chains: the AWS SDK chain (env, shared config and SSO, container or instance identity) and Google ADC. Litestream resolves them the same way. Core decides which environment variables Litestream's supervisor inherits. Record that list in
datastore-litestream.md(D1 noted 4E narrows the environment):- S3:
AWS_*,HOMEfor~/.aws,PATH; - GCS:
GOOGLE_APPLICATION_CREDENTIALS,CLOUDSDK_*,HOME,PATH.
A provider whose credentials don't come from those chains can't be replicated. Say so in the method's doc.
- S3:
Refusals: a provider without
replicaTarget(), or one that returns a target Litestream can't reach, can't host a v3 datastore. E refuses--format 3setup for it with a clear message.- Example: GCS with a custom
apiEndpoint. Litestream has no equivalent, so the GCS extension refuses inreplicaTarget()rather than returning a target that silently replicates to real GCS.
- Example: GCS with a custom
Wire it dormant. Add a resolver that turns a provider's
V3ReplicaTargetinto D1'sV3ReplicaSettingand Litestream config, used only behind the existing dormant path.SWAMP_V3_REPLICAstill wins in tests. Phase 4E switches the source.Conformance:
assertReplicaTargetConformanceinpackages/testing. The target parses,prefixmatches the datastore root thatstorage()anddatastoreControlPlaneStore()write under, and it carries no credential fields.Also extend
assertWritableDatastoreControlPlaneStoreConformanceso its namespaced-push case checks alineage/record stays at the root next towriter/. The extensions already do this in their own tests (swamp-extensions #497); the suite should hold every provider to it.
Tests
- Unit tests for the descriptor types, the resolver (S3, S3-compatible with endpoint and path style, GCS) and the refusal of a missing method.
- The conformance suites, run against the in-memory remote.
- Dormancy: nothing calls
replicaTarget()outside tests and the dormant path; a fitness rule pins it.
Blocked by: nothing. The extensions' implementation is a separate Lab issue. Phase 4E waits for both.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.