---
status: reviewed
last_updated: 2026-10-20
---
# LFS: the large-media surface
## What this is
Git LFS support for alkgit assemblies: the pointer-file flow, the
media storage placement, the door mapping, and the GC liveness wiring.
LFS is v1 scope (ADR-022) because the substrate makes it nearly free:
alkblobs' two-tier shape is exactly "small objects in kv, big media in
large tier" (local files on sqlite, pg-lo on postgres), and the media
protocol is a small batch/transfer adapter. Everything here rides the
substrate stack in [substrate.md](substrate.md); the *decision* rationale
is ADR-022.
Evidence: POC-L (`pocl-findings.md`) — real `git-lfs` 3.7.0
push/pull/fresh-consumer round trips, both door bindings, mem/sqlite/
postgres engines, GC sweep behavior. Requirements reference:
`rudolfs` (`/workspace/rudolfs`) — batch/transfer protocol behavior and
Range serving (facts only, license-clean; its no-auth posture is the
opposite of ours).
## The model in one paragraph
A pushed `git lfs` push commits **pointer-file blobs** — ordinary git
blobs under the git-object family, handled by the normal git path with
no LFS awareness. The *media* lives in alkblobs, keyed under
`Algorithm::GitObjectSha256` with the raw content as the stored
preimage: **key = `[0x04] + sha256(content)`, byte-exact the LFS oid**
(alkblobs ADR-018's preimage-identity derivation) — so the key is
formable in advance from the pointer's metadata, oid addressing needs
no translation table, and verification-on-get re-derives under the
same rule every read. Which repos reference which media is *not* a
store fact (the CAS pool is global — their ADR-017) — that reference
map is alkgit's co-tenant liveness row table, wired into the sweep
chain by default.
## The three surfaces
### 1. The batch/checking adapter (the JSON protocol)
The batch API (`POST <url>/objects/batch`) is an alkhttp-route surface
— the door maps it onto alkblobs' ops through its **own**
`OperationContext` (identity + ownership provider wired — alkcall
ADR-048's loud default is the guard against the static-fallback leak).
- Existence check: `blobs/stat` with the formable key (tag + oid).
No `have` use — that is the replication diff verb; `stat` answers
the batch's `objects[].{oid,size,authenticated}` shape.
- Authorization: the door's per-repo grant governs batch access
(ADR-011 read/write actions map to batch upload/download);
anonymous-public repos ride the door-synthesized `anon` identity
composed onto the ops ACL (POC-L's validated composition). Push is
always authenticated, as everywhere.
- `hash_algo` answers `sha256` only (409 otherwise — the batch is
pinned to the media algorithm alkgit keys on); `verify` is not
served; hrefs are door-minted absolute URLs carrying per-action
`Authorization` tokens — the LFS hand-off, client-verified.
- Batch auth accepts Basic userinfo on this surface (alkhttp ADR-073's
door tolerance) — real LFS clients authenticate the batch with
Basic userinfo; transfer actions carry the minted Bearer tokens.
### 2. Transfer (the byte protocol)
- **Upload** (`PUT <href>`): mapped onto the wire ops `blobs/put`
with the **mandatory pre-keyed claim** — key formed from the
pointer's oid, `len` from the batch's size (size always present; a
size-less offer is refused at the door). Restored to the wire by
ADR-022 §2 — the pre-keyed form is satisfiable for LFS, which is
exactly what identity-family keying buys. A mislabeled body is
refused at the stream's derivation check: no stored content, no
held pin.
- **Download** (`GET <href>`, `Range`-bearing): mapped onto the wire
ops `blobs/fetch`, ranged arm — **per-window slice digests verified
mid-flight** (the ranged arm's answer; the door owns window
splitting; measured: 45 MiB as two 32/13 MiB opens, byte-exact).
The whole arm is unbounded at establish, so the door budgets it
(per-assembly object-size ceiling); the ranged total is bounded by
`fetch_max_total_bytes` (an `OpsConfig` knob, default 64 MiB —
raise per assembly to trade opens for windows).
- **Bindings**: for the embedded door, **DirectOps** — the door binds
the ops machinery *in-process* (it holds the `OpsContext` and invokes
the op registry directly, no loopback), the recommended form (POC-L's
coined term, `pocl-findings.md` §"where do transfer/token hand-offs
bind best"; measured ~25 ms
per-request vs ~80 ms through the per-request loopback bridge);
the loopback `alk/channels` bridge stays the shape for a door that
must exercise the real ops byte-path (fleet/remote consumers) —
with the pre-keyed upload restored it now works in *both*
directions (the read-only limitation was blob-family keying's
artifact, superseded by ADR-022 §2); an in-process embedder that
binds the store directly uses the facade seam
`put_as(GitObjectSha256, len, stream)` / `read_range` with no ops
plane at all.
- Token layers compose as measured: door-minted href tokens
(per-action capabilities — `lfs:<hex>` Bearer) over alkblobs
PinTokens (store custody, `mint`/`confirm`). The hostile
upload-loop threat shape is met by the sibling's mint-boundary
admission cap (`RegistryFull` → `MINT_REFUSED` — alkblobs
ADR-019); fleet forms give pin-row TTLs by engine configuration.
### 3. The pointer lifecycle / liveness table (alkgit-owned)
One co-tenant row per reference: `(repo_id, oid, size)`.
- **Written** when a reference is asserted — at batch check/upload
time (the moment the server learns the pair) — into the same
relational store as refs/registry (substrate.md's composition; its
own transaction). No stream event: the reactive halves' publish leg
is record-write-scoped (ADR-019 — refs and liveness rows ride their
own durable surfaces; consumers of repo-record changes need neither).
- **Consumed** by alkgit's shipped LFS `LivenessSource`, wired into
the sweep chain by default (ADR-019 §6; `lfs` feature default-on —
fail-safe: an unregistered source is the dangerous default for a
mixed deployment).
- **Cross-reference sharing**: the same content in two repos is one
key with two rows; the source must re-protect the key while *any*
row references it (POC-L's refcount requirement — a crude second
scan in the POC, replaced by the shipped design; the refcount column
ships in the initial v1 schema, ADR-024 §5 — no migration owed).
- **Orphans** (rows gone from refs, content unreferenced) are
sweep-eligible: the store's sweep-window stages then deletes them
(POC-L: orphaned entry deleted, referenced survive).
- **Repo deletion**: the rows go with the repo (same removal as
refs/registry); media becomes unreferenced and sweeps — nothing
LFS-specific exists here.
- `PinConverter` stays alkblobs' sync-only/failure-free/`Key`-only
contract — rich reference metadata lives in these rows, never in
the converter (POC-L's structural fact).
## Constraints and posture
- **Engines**: LFS ⇒ a dual-tier constructor (`dual_tier_sqlite_local`
on sqlite; `dual_tier_postgres_pglo`/`+local` on postgres;
`dual_tier_mem_local` for completeness). kv-only shapes refuse
over-threshold puts — the deployment fails loudly at the first big
upload, which is the right failure (ADR-022 §6).
- **Isolation**: the CAS global-pool caveat applies unchanged — media
is cross-reachable by digest knowledge; per-repo isolation means
per-trust-domain store instances (the accepted posture, ADR-022 §7).
- **No native LFS op set of alkgit's own**: git-lfs speaks HTTP only;
the door is the alkhttp `lfs` feature (family infrastructure,
after alkgit's publish — doors.md's sequencing rule). The native
plane LFS rides is alkblobs' own `ops`.
- **Route shapes** (empirically pinned by POC-L, and the static tables
the door mounts — no route fallback — so these are the feature's
routes): batch `POST /{repo}/objects/batch` (the `/info/lfs` prefix
is a gitea URL-embedding convention, not a client rule — with
`lfs.url = http://host/<repo>` the client POSTs at `{url}/objects/
batch`); transfer `PUT`/`GET /{repo}/lfs/objects/{oid}` — hrefs are
door-minted absolute URLs pointing at these static transfer routes;
two-segment repo ids enumerate patterns (same caveat as the `git`
feature, backend.md §repo-id grammar).
- **Encryption**: non-goal (storage of pointer references and media is
custody-neutral; any at-rest encryption story rides alkvault and the
substrate's own posture, not this surface).
- **Lock server** (`lfs.locks` API): not served v1 — `lfs.locksverify
false` is the client-verified workaround; a future additive surface.
## Design Decisions
| [022](decisions/022-lfs.md) | LFS keying + door + liveness | the decisions this doc details |
| [019](decisions/019-substrate-baseline.md) | GC wiring | the sweep chain the liveness rows feed |
| [021](decisions/021-feature-graph-and-dependency-posture.md) | Feature graph | the default-on `lfs` feature (placement re-homed: `alkgit-substrate` — ADR-025, which supersedes this ADR's graph) |
## Open Questions
- **OQ-20**: per-oid pruning of stale reference rows (beyond the
repo-lifetime scoping this spec pins — the pointer-lifecycle ops'
future shape).
- **OQ-21**: the LFS lock server (`lfs.locks` API) — surface build
deferred on a client requirement (`lfs.locksverify false` is the
verified workaround); its composition-validation POC is owed before
the v0.1 publish (the register entry carries the split).
- **OQ-17** (resolved — ADR-024): the liveness refcount ships in the
initial schema; the co-tenant tables' migration story is
construction-run/version-stamped/additive-first.
## References
- [ADR-022](decisions/022-lfs.md) (the decision record — read this
first), [substrate.md](substrate.md) (the composition), [backend.md](backend.md)
(seam + repo grammar), [doors.md](doors.md) (door sequencing)
- alkblobs ADR-018 §1/§3 (preimage-identity derivation; the stat
semantic), ADR-017 there (global-pool caveat), ADR-019 there (mint
admission cap), ADR-020 there (pre-keyed wire put)
- `docs/research/pocl-findings.md` (the gate evidence, route/auth
facts, custody postures, mid-flight verification measurements)
- `/workspace/rudolfs` — the requirements reference (protocol facts
only; license-clean — see `docs/research/reference-policy.md`)