Relationships
#3534 Datastore rework Phase 4 E: v3 on S3 and GCS (solo mode): setup --format 3 on buckets, replication as a datastore property, end to end
Opened by stack72 · 10/16/2026· Shipped 10/16/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. Today v3 is opt-in for filesystem datastores only (swamp-club#3279).
Phase 4 is "solo mode on buckets": one writer at a time (the fenced lease), with Litestream replicating each checkout's local commits.db to the bucket. Everything it needs has shipped, dormant, behind the test-only SWAMP_V3_REPLICA setting:
| Issue | What | |
|---|---|---|
| B | #3358 | the pinned, hash-checked Litestream binary |
| C | #3359 | the fenced writer lease |
| D1 | #3416 | the idle sidecar; a write confirms replication before it succeeds |
| D2 | #3482 | a checkout-local commits.db copy; lineages published under _control/lineage/; restore, refresh and takeover |
| D3 | #3489 | serve confirms each write request (group commit) |
| F | #3490, #3506 | the commit outbox |
| #3491 | D2 follow-ups: migration seed, chunk-GC deferral while other lineages exist | |
| #3507 | replicaTarget(): the provider says where its replica lives |
|
| #3531 | replicaPaths(): removing abandoned lineage paths on buckets |
The extensions, @swamp/s3-datastore and @swamp/gcs-datastore, provide:
- the writable datastore-wide control plane (
datastoreWideLease), keeping_control/writer/,_control/lineage/and_control/datastore-formatat the root; storage/1;replicaTarget()andreplicaPaths()(#3508). The release that ships #3508 is this issue's minimum extension version.
Design records: design/enablers/datastore-litestream.md (Decision; Phase 4A; D1, D2 and D3; the replica target; replica paths) and design/enablers/datastores.md ("v3 …" sections).
Goal
swamp datastore setup s3|gcs --format 3 creates a v3 datastore on a bucket, and every command and swamp serve work on it end to end. It's still opt-in: v2 stays the default, and v2 datastores are untouched.
What to build
- Setup.
--format 3for extension datastores:- refuse unless the provider has
datastoreWideLease,datastoreControlPlaneStore(),storage(),replicaTarget()andreplicaPaths(). The message names the minimum extension version; - refuse on Windows while the Litestream pin is below 0.5.18 (
litestreamAvailability); - refuse a target that isn't new and empty: no v2 data under the prefix and no format marker, as
inspectV3Targetdoes for filesystem; - write the format marker through
datastoreControlPlaneStore(); - surface
replicaTarget()'s refusals as they are (for example GCS with a customapiEndpoint).
- refuse unless the provider has
- Replication is a property of the datastore. For a v3 bucket datastore, the replica comes from
resolveV3ReplicaSettingwith the provider's target, not fromSWAMP_V3_REPLICA(which stays, test-only, and still wins in tests).- Every checkout replicates. A checkout can't turn it off, because one that didn't would write a
commits.dbnothing reads. - Remove the dormancy pins in
integration/datastore_v3_rules_test.tsthat keepresolveV3ReplicaSetting,resolveV3ReplicaPathsandreplicaTarget()out of production. Pin the new production callers instead.
- Every checkout replicates. A checkout can't turn it off, because one that didn't would write a
- Chunks go through the provider's
storage(), and chunk GC lists and deletes through it with its grace period. Collection runs once #3491's deferral lifts, that is once no other lineage is registered. - The lease and the lineage records use
datastoreControlPlaneStore()at the datastore root. - Lineage path GC on buckets uses
replicaPaths()(#3531), now wired in production. - Narrow the sidecar's environment to the table in
datastore-litestream.md("the replica target"): credentials,HOME,PATH, proxies and CA bundles. D1 passes the whole environment today. - No v2 sync on a v3 bucket datastore. The extension's file sync service,
datastore syncand v2's pollers don't run. Refusedatastore syncwith the same message as filesystem-v3. - Second machines join through the clone route, as on filesystem-v3: the repo's
.swamp.yaml, then a first command restores from the current lineage (D2). Make the first-clone cost visible: one check, plus one restore before the first command. - Retention (swamp-club#3473): the CLI's hourly end-of-command pass can run at the end of a read-only command and commit. Its replication sync must never fail the user's command. Retention swallows its own
datastore_v3_replication_unconfirmedand logs it; the user's own commits still fail as today. - Serve on a v3 bucket datastore: the lease for its lifetime, D3's per-request confirmation, the hourly maintenance timer, and retention through
V3MaintenanceTimer. - Docs:
design/enablers/datastores.md"v3 opt-in", the setup command's help, the extension READMEs (the minimum version), and the bundled skill's datastore references. Runverify-skills.
Already handled (don't redo)
- Migration of an existing shared
commits.dbinto the first lineage: #3491. Not needed for buckets, since a v3 bucket datastore is always new. - The pre-4E list from D2: #3491.
- Real-endpoint timings: deferred to Phase 4 dogfooding (I), per swamp-club#3417.
Tests
- Integration on the in-memory remote (
packages/testing) withstorage,replicaTarget(a file replica under the test's temp root) andreplicaPaths:- setup refusals;
- two checkouts: write, clone, restore, takeover, zombie fenced;
- chunk GC, then lineage path GC;
- retention's sync failure not failing a read command;
datastore syncrefused.
- The opt-in cases against ministack and fake-gcs-server, as D1 and D2 have them.
- v2 doesn't change; apply the v2 gate.
- swamp-uat's
s3-v3andgcs-v3backends (Phase 4 H) follow this issue. Note anything H needs.
Blocked by:
- the extension release with swamp-club#3508 (the minimum version);
- swamp-club#3532 (v3 version numbers never go back), because several machines sharing one datastore is exactly where reused version numbers show.
Everything else it needs has shipped.
Shipped
Click a lifecycle step above to view its details.
stack72 commented 10/16/2026, 3:11:43 PM
Addition from the #3508 review: when E narrows the Litestream sidecar's environment (item 6), pass STORAGE_EMULATOR_HOST through when it is set. #3507's table leaves it off as test-only, but it's the only way Litestream reaches a GCS emulator, and swamp-uat's GCS jobs (and Phase 4 H's gcs-v3 backend) all run on fake-gcs-server. The extensions are being asked to accept an apiEndpoint when STORAGE_EMULATOR_HOST names the same host; without this pass-through the sidecar still couldn't reach it. Add an integration case on fake-gcs alongside the ministack one.
stack72 commented 10/16/2026, 3:32:25 PM
Minimum extension version: @swamp/s3-datastore and @swamp/gcs-datastore 2026.10.16.1 (swamp-extensions #504, swamp-club#3508 + #3531). It's the first release with replicaTarget() and replicaPaths(), alongside storage() and the datastore-wide control plane. It also accepts a GCS apiEndpoint when STORAGE_EMULATOR_HOST names the same host and port (test-only, for Phase 4 H). E's setup refusal should name 2026.10.16.1.
stack72 commented 10/16/2026, 6:44:34 PM
Two items from #3549's planning (extensions guard for pre-guard binaries): (1) please add a test that on a v3 bucket datastore no sync-service method (pullChanged / pushChanged / preparePush / commitPush / markDirty) is ever called. The in-memory remote's recording sync service shows it. #3549's extension refusal relies on that invariant. (2) If #3549's extension release lands before this PR merges, raise V3_BUCKET_MIN_EXTENSION_VERSION to that release, so every v3 bucket repo's lockfile pins an extension with the guard from day one.
stack72 commented 10/16/2026, 9:03:41 PM
#3549 has shipped: @swamp/s3-datastore and @swamp/gcs-datastore 2026.10.16.5 (swamp-extensions #508) exclude _litestream/ from the file sync and refuse a format-3 marker at the first pull or push. Since it landed before this PR, raise V3_BUCKET_MIN_EXTENSION_VERSION to 2026.10.16.5 (and the setup refusal message and docs with it), so every v3 bucket repo's lockfile pins an extension with the guard from day one.
Sign in to post a ripple.