07 — File Versioning
Topic: file versioning. Versioning in a content-addressed store is almost free for storage and almost entirely a metadata and retention problem. This doc designs the version model, restore, retention, immutability/WORM, snapshots, and the interaction with dedup and GC. Decision context in ADR-0020 placement is adjacent; the version model itself is recorded here and in 08 data model.
1. Model: immutable, content-addressed versions (copy-on-write at chunk level)
A Version (owned by the File & Metadata context, 08) binds
a node to one Object (content_hash + manifest, 02).
Versions are immutable and form an append-only chain per node; the node carries
a current_version_id pointer.
flowchart LR
classDef v fill:#dbeafe,stroke:#1e40af,color:#111827;
classDef m fill:#fde68a,stroke:#b45309,color:#111827;
classDef c fill:#bbf7d0,stroke:#15803d,color:#111827;
v1["v1"]:::v --> m1["manifest1<br/>[A,B,C]"]:::m
v2["v2"]:::v --> m2["manifest2<br/>[A,B,C2]"]:::m
v3["v3 (current)"]:::v --> m3["manifest3<br/>[A,B2,C2]"]:::m
m1 --> A["chunk A"]:::c
m1 --> B["chunk B"]:::c
m1 --> C["chunk C"]:::c
m2 --> C2["chunk C2"]:::c
m3 --> B2["chunk B2"]:::c
m2 -. shares .-> A
m2 -. shares .-> B
m3 -. shares .-> A
m3 -. shares .-> C2
The win: editing one region of a file changes only the CDC chunks in that region (02); the new version’s manifest shares every unchanged chunk with prior versions. A 1-byte edit to a 1 GiB file stores ~one new 1 MiB chunk, not 1 GiB. Versioning cost = the delta, achieved implicitly by chunk dedup — no explicit diff format needed.
2. Restore, copy, and conflict copies (all cheap metadata ops)
- Restore vN: create a new version whose manifest = vN’s manifest (or move the
currentpointer). No bytes move; chunks are already present (refs++). Restoring is forward-only (history is never rewritten), preserving the audit chain. - Copy a file: new node/version referencing the same manifest/chunks (refs++). Byte-free (invariant I5, 08).
- Sync conflict copies (ADR-0008): a conflicted copy is just a sibling node/version pointing at the loser’s manifest — no special storage, both histories fully recoverable.
3. Retention policies (the part that actually costs)
Infinite history is infinite metadata (and eventually storage, as old unique chunks pile up). Retention bounds it. Policies are per-tenant/per-folder:
| Policy | Rule | Use |
|---|---|---|
| Keep-last-N | retain newest N versions | default user files |
| Time-based | retain versions for D days | compliance windows |
| Tiered thinning | keep all for 7d, daily for 30d, weekly for 1y | space-efficient long history |
| Significant-only | keep versions marked/large/explicit | reduce churn noise |
| Legal hold / WORM | retain immutably until released | compliance (§5) |
When a version is pruned by policy, its manifest is dereferenced; chunks unique to it become unreferenced and are reclaimed by GC after the grace period (11). Chunks still shared with retained versions survive — pruning never corrupts a kept version.
4. Snapshots (point-in-time, tenant/folder scope)
A snapshot is a named, immutable set of (node → version) bindings at an
instant — essentially a manifest-of-manifests at the namespace level. Because
everything is content-addressed, a snapshot is pure metadata: it pins existing
versions (refs++), storing no new bytes. Snapshots enable “restore my whole folder
to last Tuesday” and are the unit a retention policy or legal hold can target.
Non-goal reminder: snapshots are not a system backup/DR product (NG in 02 product goals); they are user-facing point-in-time recovery of namespace state. System DR is operational backup of Postgres + object storage (08 metadata).
5. Immutability, WORM & legal hold
- Versions are immutable by construction (content-addressed); you cannot alter a version, only supersede it.
- Compliance retention (WORM): for regulated tenants, manifests/packs backing
held versions are written to Object-Lock / immutability-policy buckets
(01 capability matrix)
so even an operator cannot delete them before the retention expires. The chunk
index records a
retain_untilfloor that GC must respect (11). - Legal hold sets an indefinite floor until explicitly released; it overrides normal retention pruning.
6. Tradeoffs / Alternatives / Scaling
Tradeoffs. Content-addressed manifests make version storage cheap but shift the cost to metadata (a version row + a manifest per save) and to GC (pruning must compute which chunks became unreferenced). We accept metadata growth and bound it with retention + manifest nesting for huge files.
Alternatives considered.
- Whole-object versioning (S3 Versioning style): every version is a full copy — no chunk sharing. Simple, but storage cost = sum of all versions; pathological for large frequently-edited files. Rejected; chunk sharing is strictly better.
- Reverse-delta chains (store latest + diffs backward, like some VCS): space- efficient but restore of old versions requires replaying diffs (rehydration cost), and a corrupt link breaks the chain. Content-addressed manifests give delta-level savings without chain rehydration — any version resolves directly. Preferred.
- Forward-delta / xdelta per version: couples versions, complicates dedup across unrelated files. Rejected.
Scaling concerns.
- Version/manifest row growth is the real scaler: a heavy editor can create thousands of versions. Retention policies + tiered thinning cap this; manifests are small and themselves dedup (identical content → identical manifest stored once).
- Pruning at scale must be incremental and not block writes → handled by the async GC mark-sweep (11), never inline on save.
- Snapshot fan-out: a tenant snapshot pins millions of versions — stored as a compact range/bitmap over version ids, not millions of edge rows, to keep snapshot cost sub-linear where possible.
- Hot current-pointer: the node’s
current_version_idis the contended field on rapid saves; it lives on the namespace row (File context) and is updated in the same commit transaction as the new version (05 §4).
References
- S3 Versioning (whole-object, contrast): https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html
- S3 Object Lock (WORM): https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html
- Git tree/blob content-addressed history (model analogy): https://git-scm.com/book/en/v2/Git-Internals-Git-Objects