OXYGENHUB
# 4f22a8fd0ed4 oxygen / docs / stores.md
8.8 KiB · Markdown · last changed Name the storage feature Oxygen Strata (refs #63) 1 month ago renderedsource blame history raw
1
# Oxygen Strata: pluggable storage
2
3
Status: design, 2026-10-03. Nothing here is built yet. Next to implement.
4
5
**Oxygen Strata** is the feature: a repository can live on any storage layer. In code and in this document, each backend is a **store**.
6
7
## 1. What and why
8
9
Today an Oxygen repository lives in `.oxy/` on a disk, and a remote is
10
either another directory or an OxygenHub. This feature makes storage
11
pluggable. Any **store** that can keep write-once objects and update a
12
small reference record by compare-and-swap can hold a complete
13
repository, with no server in between.
14
15
The first stores are **Amazon S3** and **Google Cloud Storage**, built
16
side by side so the code stays portable. Azure Blob and a Git repository
17
used as a plain store follow on the same interface.
18
19
Why Oxygen can do this safely when Git cannot:
20
21
- **Objects never change.** Changes, states, blobs, attestations and
22
  receipts are named by their BLAKE3 hash and written once.
23
- **Only references are mutable.** Bookmarks, tags and the operation
24
  head are small, and object stores now offer compare-and-swap on a
25
  single object.
26
- **Every read is verified.** Objects are re-hashed on read and changes
27
  are signed, so a bucket is untrusted storage. Tampering is detected,
28
  and nobody can forge history, not even the bucket's owner.
29
30
Performance is "good enough" for one engineer today. This work is about
31
where a repository can live, not about speed.
32
33
## 2. What users see
34
35
Storage must be transparent: a bucket behaves like any other remote, with
36
no new concepts to learn.
37
38
```sh
39
oxy remote add backup s3://my-bucket/repos/oxygen
40
oxy push backup
41
oxy clone gs://team-bucket/metalgear
42
oxy pull
43
```
44
45
- **URLs:** `s3://BUCKET/PREFIX` and `gs://BUCKET/PREFIX`. The prefix is
46
  optional, so one bucket can hold many repositories.
47
- **Credentials come from the standard places**, with no oxy-specific
48
  configuration:
49
  - **AWS:** environment variables, `~/.aws` profiles (`AWS_PROFILE`),
50
    SSO, and instance or container roles;
51
  - **Google Cloud:** Application Default Credentials (`gcloud auth
52
    application-default login`), service account files, and workload
53
    identity.
54
  - **Overrides:** optional per-remote settings in the repository's
55
    config (`profile`, `region`, `endpoint`), for example
56
    `oxy remote add backup s3://b/p --profile work`.
57
- **Errors are written for people.** Each one says what to do next, for
58
  example: "no AWS credentials found: run `aws sso login` or set
59
  AWS_PROFILE", or "the bucket exists but this account cannot write to
60
  it (s3:PutObject denied on my-bucket/repos/oxygen)".
61
- **`oxy init --remote s3://…`** creates an empty repository in the
62
  bucket, and `oxy remote check NAME` reports access, latency and
63
  what is stored there.
64
- **Progress, colour and glyphs** follow the CLI style of every other
65
  command.
66
- **Several writers are safe.** Two people pushing to the same bucket at
67
  once is handled the way the operation log handles two processes today
68
  (§5).
69
70
## 3. Architecture
71
72
- **A `Store` trait in oxy-core**, with three implementations: the
73
  current local `.oxy/` store, S3, and GCS. It has two halves:
74
  - **objects**: `has`, `get`, `get_range`, `put` (create only), and
75
    `list` by prefix;
76
  - **references**: `read() -> (bytes, version)` and
77
    `write(bytes, expected_version) -> Ok | Conflict`.
78
- **The `object_store` crate** (Apache Arrow) for S3 and GCS: one API
79
  for both, mature credential chains, multipart uploads, and conditional
80
  puts. Azure is in the same crate, so it comes nearly free later.
81
- **Push and pull talk to the bucket directly** through the same
82
  negotiation the hub protocol uses: what does the remote have, and what
83
  is missing? The client sends packs, never loose objects.
84
- **A local cache** of fetched objects, which is always correct because
85
  objects never change.
86
- **The working repository stays local.** A bucket is a remote. Running
87
  a repository *from* a bucket (no local `.oxy/`) is out of scope here;
88
  it is what the virtual working copy (§8) is for.
89
90
## 4. Layout in the bucket
91
92
```
93
PREFIX/
94
  oxygen.toml          format version and repository id, written once
95
  packs/<id>.pack      immutable, zstd-compressed objects
96
  packs/<id>.idx       sorted index of the pack (read with range GETs)
97
  refs                 bookmarks, tags and the remote's operation head: one object, updated by compare-and-swap
98
  receipts/, attest/   only if they are kept outside packs
99
```
100
101
- **Pushing** uploads one pack and its index, then updates `refs`.
102
- **Packs never change.** A pack named in `refs` is complete, because
103
  the pack is written before `refs` moves.
104
- **One `refs` object** so that a push moving several bookmarks is
105
  atomic. Per-reference objects would scale to more writers but lose
106
  atomic pushes; one engineer and small teams do not need them.
107
- **Many small packs from many pushes** are merged by `oxy gc` against
108
  the remote, as `merge_packs` does locally.
109
110
## 5. Concurrency: compare-and-swap on `refs`
111
112
| Provider | Create only | Replace only if unchanged |
113
|---|---|---|
114
| S3 | `PutObject` with `If-None-Match: *` | `PutObject` with `If-Match: <ETag>` |
115
| GCS | `ifGenerationMatch=0` | `ifGenerationMatch=<generation>` |
116
117
When a push finds `refs` has moved, it re-reads `refs` and merges by the
118
same rules as the operation log's merge:
119
- a bookmark moved by only one side takes that side's value;
120
- moved differently by both, it keeps both sides' heads, as several
121
  heads;
122
- a tag keeps its first value.
123
124
It then retries the write. Uploading packs needs no locking: two writers
125
uploading the same objects is harmless.
126
127
Before building on it, check each provider's current documentation for
128
conditional writes: S3 added `If-Match` on `PutObject` in 2024, and this
129
design depends on it.
130
131
## 6. Safety
132
133
- **The bucket is untrusted.** Every object is re-hashed on read, as
134
  local reads are now. Signatures and repository policy are checked on
135
  fetch exactly as for a hub remote.
136
- **Bound to one repository.** `oxygen.toml` names the repository id. A
137
  push to a bucket holding a different repository is refused, and so is
138
  a clone that finds packs naming another id.
139
- **Size limits.** The existing 1 GiB object cap, plus limits on index
140
  and `refs` sizes, so a hostile bucket cannot drive huge allocations.
141
- **Garbage collection never deletes in place.** `oxy gc` on a remote
142
  writes the merged pack and moves `refs` first. Packs no longer named
143
  are deleted only after a grace period (default 7 days), so a slow
144
  concurrent reader is never left with a missing pack.
145
- **Least privilege.** The documented IAM and GCS policies need only
146
  Get, Put, List and Delete under the prefix. A read-only key gives a
147
  working clone and pull.
148
- **No secrets in the repository.** Credentials stay in the providers'
149
  own chains or the OS keychain, never in `.oxy/config`.
150
151
## 7. Testing
152
153
- **One conformance suite** that every store must pass, run against
154
  local, S3 and GCS. It covers object round trips, range reads,
155
  create-only refusals, compare-and-swap conflicts, concurrent pushers
156
  merging, an interrupted push leaving the remote valid, gc with a
157
  concurrent reader, and a repository-id mismatch being refused.
158
- **Emulators in CI** (Flow jobs have no network):
159
  - MinIO for S3, if its conditional writes match S3's, otherwise
160
    LocalStack;
161
  - `fake-gcs-server` for GCS.
162
- **Real buckets** (Andy's AWS and GCP accounts) for a nightly or
163
  manual test run. It needs network access, so it runs outside the
164
  isolated tier, with credentials scoped to a test prefix.
165
166
## 8. Order of work
167
168
1. **The `Store` trait in oxy-core**, with the local store moved behind
169
   it and no change in behaviour, so every existing test passes.
170
2. **S3 and GCS stores together**, with the conformance suite and the
171
   emulators.
172
3. **`oxy remote add`, `push`, `pull`, `clone` and `init --remote`**
173
   for bucket URLs, with the friendly errors and `remote check`.
174
4. **`oxy gc` against a bucket**, with the grace period.
175
5. **Documentation:** a user guide page, sample IAM and GCS policies,
176
   and a README section.
177
178
Later, on the same trait:
179
- Azure Blob storage;
180
- a Git repository used as a plain store, so `oxy push` works to
181
  GitHub or GitLab;
182
- the hub attaching a repository from a customer's bucket ("bring your
183
  own storage", see oxygenhub `docs/scaling.md`);
184
- couriers: signed, encrypted bundles shared by email, Slack, Matrix
185
  and the like (a separate design);
186
- a virtual working copy mounted with FUSE (from OxygenFlow's
187
  `flow-casfs`).
188
189
## 9. Open questions
190
191
- Should `oxy clone s3://…` set the bucket as `origin`, or prompt for a
192
  name? Proposed: `origin`, as for any URL.
193
- Does a bucket remote keep its own operation log, or only `refs`?
194
  Proposed: only `refs` at first. The local operation log already
195
  records what each push did.
196
- Should packs be encrypted on the client, so the provider only ever
197
  stores ciphertext, beyond the provider's own encryption? Proposed:
198
  later, as an option. It does not change the layout.