openxet — Design
Date: 2026-10-23 · Status: approved; reconciled with the M0 spike results
1. Intent
Hugging Face's Xet storage gives chunk-level deduplicated storage for large,
versioned files. Models, datasets and checkpoints are the common examples, not
the definition. The client side is open source
(huggingface/xet-core, Apache-2.0) and the protocol is published
(Xet Protocol Specification v1.1.0, https://huggingface.co/docs/xet). The server
is not. Nobody can run Xet on their own infrastructure.
openxet is the open-source, self-hostable server for the Xet protocol. The
official, unmodified Hugging Face clients (hf_xet, huggingface_hub /
hf CLI, git-xet, @huggingface/hub) work against it. Git repos can live
on any Git host (GitHub, GitLab, Gitea and others) and keep their large files
in openxet (§6.1). It runs on storage
and identity systems people already own.
It is not a Hugging Face copy. It is plug-and-play infrastructure: small surface, clear seams, no opinions forced on the operator.
Who it is for
- Organizations that cannot or will not put data on huggingface.co (regulated, sovereign, air-gapped, cost-sensitive) but want HF-style workflows.
- Teams who today keep large files in MinIO + Git LFS / DVC and get no cross-version deduplication.
- Platforms that host other people's large files, such as models and datasets (aqora is a likely first user), and want Xet storage under their own product, with their own naming, permissions and UI.
What success looks like
- From zero to a working
hf uploadagainst openxet in under two minutes: one command, one environment variable. - Uploading a slightly changed 10 GB checkpoint transfers megabytes, not gigabytes.
- It scales by adding nodes, with no redesign, from a laptop to petabytes.
- Operators recommend it because it is boring to run.
- People who don't want to run anything get the same thing, done perfectly, on openxet.com (§15).
1a. User value model (decided by the user)
What users get from openxet, and how each value is measured. The targets are initial, like §5.8's, and revised with data. Each is checked by the benchmark suite or the compatibility suite (§8), and the measured numbers are published with every release. "Checked in" says where: targets that fit a CI runner gate every PR, and those that need hosted infrastructure, 10 GbE hardware or an external score (OpenSSF Scorecard) run on the release benchmark rig or as a release check.
| Dimension | What it means to the user | How openxet delivers it | Target | Checked in |
|---|---|---|---|---|
| Speed | Uploads, downloads and listings never wait on openxet | Direct presigned downloads (ADR 0002), stateless nodes, a fast index, the cache tier (§10.5) | At 1,000 requests/s per node, against an index of 10⁷ files and 10⁷ xorbs: reconstruction p99 < 50 ms for files up to 100k terms; token, head and resolve p99 < 10 ms (§5.8). ≥ 9 Gbit/s sustained xorb ingest on one node | Release rig |
| Dedup and efficiency | Only changed bytes move or get stored | Client-side chunking with global dedup; server-side chunking for every other client (§10.3); our client's Git LFS download agent (§6.1) | A 1 MB edit of a 10 GB file transfers ≤ 2 MB up with stock hf_xet, also from a second machine with an empty cache; ≤ 2 MB down with our client when the previous revision is in the local Xet chunk cache (§10.8 can seed it from an existing LFS checkout); storage overhead over unique compressed data ≤ 5% | Per-PR CI |
| Compatibility | The tools they already use just work, in both directions with huggingface.co | Byte-exact formats on xet-core's crates (ADR 0001); the Hub and LFS surfaces of §6 | 100% of the compatibility suite passes for the oldest supported, pinned and latest clients; zero divergence on the golden vectors | Per-PR CI; latest clients also nightly |
| Reliability and integrity | Acknowledged data is never lost, corrupted or silently dropped | The bucket is the source of truth (§5.3); every xorb and shard is verified on ingest (§5.9); deletion only through GC (§5.6) | Zero acknowledged writes lost under SIGKILL fault injection; reindex from the bucket alone restores every file, with refs at most 60 s stale (RPO); verify has zero false positives and exits non-zero on real corruption | Per-PR CI |
| Time to value | Working in minutes, with nothing to learn first | One binary, zero-config dev mode (§7), copy-ready commands | First hf upload of a ≥ 1 GiB file < 2 min from a clean machine self-hosted, < 60 s from the landing page on openxet.com (§15) | Release check (rig for self-hosted, the hosted service for openxet.com) |
| Control and sovereignty | Their bucket, region and identity system; no lock-in | Storage and auth adapters (§5.3, §5.4); pools and residency (§10.7); bring-your-own bucket on openxet.com | No byte crosses a pool boundary (residency test); data written by openxet reads with stock clients and huggingface.co tooling, and the reverse | Per-PR CI |
| Security and privacy | Only the right people read or write, and nothing leaks | The requirements of §5.9, each with its test | Every §5.9 test passes; OpenSSF Scorecard ≥ 9 (§16) | Per-PR CI; Scorecard as a release check |
| Reproducibility | A revision always returns the same bytes | Content-addressed files and Git commit OIDs; data keyed by commit is immutable | A download by commit OID is byte-identical across nodes, cache states and consecutive releases (sha256-checked in the upgrade tests) | Release check (upgrade tests) |
| Cost transparency | Users see what they store, what dedup saved and what it costs | Logical and physical metering per scope and tenant (§5.7), shown in the web UI and --json | The sum of metered physical bytes across tenants is within 1% of bucket bytes, excluding objects pending GC; per-tenant physical bytes are exact for dedicated-bucket and per-scope tenants; savings shown on every repo page | Per-PR CI |
| Agent UX | Agents use every surface without a human in the loop | §7.1: --json with versioned schemas, stable exit codes, structured errors with a hint, MCP | The agent leg passes with zero prompts, zero unparseable outputs and zero schema breaks | Per-PR CI |
Where openxet beats huggingface.co:
- Dedup for every client. Our client fetches only missing chunks on
git clone/git pull(§6.1), and server-side chunking dedups uploads from clients that don't speak Xet (§10.3). - Your own bucket and region, plus caches near your GPUs (§10.5, §10.7).
- Mount and stream any revision (§10.8).
- Savings shown to the user, in bytes and money (§10.10, §15).
Explicit trade-offs:
- Dedup vs read fragmentation. The more a file dedups against others,
the more xorbs a read touches. Stock clients already skip dedup of very
short ranges (
xet-data1.7.0,deduplication/defrag_prevention.rs), the cache tier offsets the rest, and the benchmark suite tracks ranges per GB read. - Global dedup vs privacy. Global dedup reveals whether a chunk
already exists, even through HMAC-keyed answers.
per-scopededup (§5.4) removes that signal and costs cross-tenant savings. - Direct vs proxied downloads (ADR 0002). Direct presigned URLs save node bandwidth but need a store that binds a signature to one range. Proxied reads work everywhere and cache, at the cost of node bandwidth.
- Client-side vs server-side chunking. Client-side chunking saves bandwidth; server-side chunking covers every client but costs server CPU and a full transfer.
2. Goals, non-goals, anti-goals
Goals (v1)
- Protocol-exact Xet CAS server. It implements the spec's CAS API and produces/accepts byte-identical hashes, xorbs and shards.
- Hub-compatibility layer. It implements just enough of the Hub HTTP API
that
huggingface_huband thehfCLI work unmodified viaHF_ENDPOINT: create repo, upload, commit, download, list, branches and tags. - Bring your own everything. Storage, database and identity are adapters.
- Horizontal scalability from day one.
- Fast. Speed is a measured target, with benchmarks gating CI.
- Easy to operate. One binary, one config file, sane defaults, metrics, and recovery from the bucket alone.
Non-goals (v1)
- No HF-style product features: model cards, discussions, likes, search, running Spaces or inference. The only web UI is minimal self-service (§10.10).
- No user accounts, passwords or user database. openxet never owns users.
- No custom distributed object store (§5.3). This is a principle, not a deferral: durability is what object stores are for.
Scope rule (decided by the user): be ambitious. Nothing in this design is deferred for convenience. Everything below the principles above ships in 1.0.
Anti-goals (failure even if it technically works)
- Slow, at upload, download or metadata operations.
- Hard to set up: more than one command and one variable to first upload, confusing config, or mandatory extra services.
- Not listening to users: feedback that has nowhere to go or is not acted on.
- Opinionated all-in-one: forcing its own auth, naming, UI or storage on people.
- Silent incompatibility: anything that makes data written by openxet unreadable by stock Xet clients, or vice versa.
- Losing data. Deletion happens only through explicit, auditable garbage collection with a grace period.
- AI slop: speculative abstractions, comment noise, dead code, untested paths, docs that drift from behaviour.
3. Constraints and verified facts
These were checked against primary sources on 2026-10-23
(spec at huggingface.co/docs/xet; huggingface_hub main @ 03b2828;
xet-core main), and by the M0 spikes against huggingface_hub 2.2.0 and
hf_xet 1.7.0 (§11). Re-verify before relying on them; the compatibility
test suite (§8) is the lasting source of truth.
-
CAS API (spec "api" page):
GET /v2/reconstructions/{file_id}(preferred; clients fall back to v1 on 404/501),GET /v1/reconstructions/{file_id}(deprecated),POST /v1/reconstructions(batch),GET /v1/chunks/default-merkledb/{hash}(global dedup, returns a shard),POST /v1/xorbs/default/{hash},HEAD /v1/xorbs/default/{hash},POST /v1/shards,POST /v2/shards(NDJSON progress stream; clients fall back to v1 on 404 or 501),HEAD /v1/files/{file_id},GET /v2/file-chunk-hashes/{file_id}(delta uploads). Hash-to-hex conversion reverses bytes within each 8-byte group, which is a classic source of bugs. -
What stock
hf_xet1.7.0 actually sends (S6, source and observed):- Global dedup queries go to
GET /v1/chunks/default/{hash}, not the spec'sdefault-merkledb. openxet serves both. - It sends
POST /v1/telemetry. openxet accepts it and drops the body: nothing is stored or forwarded (no telemetry, §7). - It tries
POST /v2/shardsfirst and falls back to/v1/shardson 404 or 501. - An empty file is a file entry with 0 segments, an all-zero file hash and an all-zero sha256.
- The wasm build uploads shards with lookup tables and a footer, although the spec says the footer must be omitted. openxet accepts a consistent footer and discards the tail (S6 rule 3).
- Global dedup queries go to
-
The CAS wire contract (S10,
hf_xet1.7.0, source and observed):- A storage-only upload and download uses seven routes: dedup query,
xorb upload,
/v2/shardsand/v1/shards,/v2/and/v1/reconstruction, and telemetry. Nothing else is needed. - Every write is retried as a whole on
5xx,408,429and timeouts, up to 6 attempts, so xorb and shard uploads must be idempotent. 404and501on a/v2/route make the client fall back to/v1/. A content failure on/v2/shardsis therefore an in-streamerrorevent, never a status.- A reconstruction whose range starts at or past EOF answers
416. - Data URLs are fetched with a single
Rangeand no credential. - The client never reacts to
401; it refreshes tokens only by clock. - Stock clients cache shards for 21 days and dedup against them without asking the server, which sets the GC grace floor (§5.6).
- A storage-only upload and download uses seven routes: dedup query,
xorb upload,
-
Shard sha256 is stored in the hash layout: the raw bytes are the digest with each 8-byte group reversed. It is client-asserted; the server can't check it without the file's bytes, so the sha256 → Xet hash mapping is scoped to the repo whose writer uploaded it (§5.4).
-
Shard verification (S6): twelve rules, all checkable from the chunk hashes and lengths recorded at xorb ingest, never by reading a xorb back.
verify_shardcaps the referenced chunks per shard (MAX_REFERENCED_CHUNKS, default 2^24, about 1 TiB of logical data) and the distinct referenced xorbs (MAX_REFERENCED_XORBS, default 4096, which bounds the chunk tables the caller loads), and rejects shards that reference missing xorbs. Xorbs the CAS info section lists but no segment references are ignored. Reference and realhf_xetshards pass. -
Auth: clients obtain
{accessToken, exp, casUrl}fromGET {HF_ENDPOINT}/api/{repo_type}s/{repo_id}/xet-{read|write}-token/{revision}. The CAS token is scoped to repo and ref, andwritesupersedesread. The token routes must return the token twice: in the JSON body and as headersX-Xet-Cas-Url,X-Xet-Access-Token,X-Xet-Token-Expiration.huggingface_hubreads only the headers andhf_xetonly the body (S3). Clients refresh about 30 s beforeexp. -
Download discovery:
HEAD/GET /{repo}/resolve/{rev}/{path}returnsX-Xet-Hashplus aLink: <…>; rel="xet-auth"(orX-Xet-Refresh-Route) header. If the redirect is followed, plain HTTP content comes back. -
Upload flow in
huggingface_hub, whenhf_xetis installed (the default):POST /api/{type}s/{id}/preupload/{rev}decides "regular" vs "lfs" per file.- The Xet upload runs with a write token;
hf_xetcomputes sha256 during chunking. POST /api/{type}s/{id}/commit/{rev}sends NDJSON that references large files by sha256 oid.
So the Hub layer must map sha256 to Xet file hash. The shard's file metadata carries the sha256.
-
Uploads go through the server today; direct xorb uploads are coming. Stable clients up to
hf_xet1.7.0 POST xorb bytes to the CAS, which verifies them. Upstream is adding xorb upload grants (XorbUploadGrant, researched 2026-10-23):- Shape.
POST /v1/xorb-grants/{prefix}/{hash}with{"length", "checksum": {"algo": "crc64nvme", "value": "<decimal>"}}answers201with a grant{id, method, url, headers}, or200if the xorb exists. The client sends the bytes tourlwith exactly those headers and no CAS credential, then commits withPOST /v1/xorb-commits/{prefix}/{hash}/grant/{id}. A404commit means no data arrived, and the client asks for a new grant. - Fallback. A
404on the grant request makes the client usePOST /v1/xorbsinstead, and so does running out of re-grants (HF_XET_CLIENT_MAX_XORB_GRANT_ATTEMPTS, default 3). Any other error fails the upload. - Browser (wasm) clients use grants too, so grant URLs need CORS.
- Who uses it.
xet-coremainhas only the types. The client flow is on tagv1.8.0-dev0, shipped as the PyPI pre-releasehf-xet1.8.0.dev0 (2026-10-20), where it is on by default (HF_XET_CLIENT_LEGACY_DIRECT_XORB_UPLOAD=1turns it off). No stable release uses it;hf-xet1.7.0 has no grant code (checked in the published wheels).
openxet answers
/v1/xorb-grants/*with404until it supports grants, never501, so these clients fall back. How it will support them is in §5.5. - Shape.
-
Downloads can bypass the server (verified by spike S1,
docs/research/S1-S2-direct-downloads.md; decided in ADR 0002). v2 reconstruction returns signed URLs, whichhf_xettreats as opaque.hf_xet1.7.0 splits multi-range fetch entries into single-range GETs by default. WithHF_XET_CLIENT_ENABLE_MULTIRANGE_FETCHINGon, it sends a multi-range header. Presigned MinIO answered that with the whole object (observed withhf_xet). Anonymous public-object GETs on AWS S3 and Azure also returned the whole object, and GCS rejected them; presigned behaviour on those three is unverified. openxet therefore emits one range per fetch entry, each with its own range-bound URL. Stockhf_xetdownloads correctly from such SigV4 presigned URLs on MinIO. -
Global dedup: only "eligible" chunks are queryable: the first chunk of each file, plus chunks whose hash's last 8 bytes (LE u64) % 1024 == 0. Responses are shards whose chunk hashes are HMAC-keyed. This keeps the global dedup index about 1/1000th of the chunk count, which is what makes scale tractable.
-
huggingface_hubnow knows abucketrepo type (HF Storage Buckets: mutable, no git revisions). openxet serves it in v1 (§10.4). -
xet-corecrates (xet-core-structures,xet-data,xet-client) are Apache-2.0 and contain the reference merklehash, xorb, shard and chunking code.
4. Decisions made with the user
- Build both the storage core and the Hub-compat layer in v1, as two cleanly separated parts of one product. The storage core is usable alone.
- Plug-and-play over all-in-one; not an HF clone.
- openxet does not own users; identity and permissions come from the operator's existing systems.
- Scaling is designed in from the beginning.
- Storage is an adapter over existing object stores, not a built-in distributed store. Local disk is a first-class backend for single-node and dev.
- Rust. Apache-2.0.
- Quality bar: excellent
AGENTS.md, best practices, useful tests kept current, current docs, check git history before changes to avoid regressing past decisions, deliberate distribution, no AI slop, user-obsessed. - Our own client is in v1, and must stay 100% compatible with the official tools and huggingface.co (§10.8).
git-xetand Git over SSH are core, v1 (§6.1, §10.1).- Build straight to 1.0 with no alpha pause (§12).
- Be ambitious: nothing is deferred for convenience. That brings in
server-side chunking, all repo types including
bucket, the cache tier, the HF pull-through mirror, multiple storage pools and residency, and the full distribution list (§9, §10). - openxet issues revocable access tokens (and SSH keys) bound to external identities (§5.4).
- The name is openxet.
5. Architecture
5.1 Shape
git · git-lfs · git-xet · hf CLI / huggingface_hub / hf_xet · huggingface.js · openxet client · browser
│ Git (HTTP/SSH) │ Hub API · LFS batch │ CAS API │ xorb ranges │ web
▼ ▼ ▼ ▼ ▼
┌──────────────────────────────── openxet (stateless nodes, one binary) ─────────────────────────┐
│ git + ssh │ hub layer │ cas core │ transfer │ web (optional) │
│ upload/ │ repos/refs/commits │ xorb/shard verify, │ signed-range reads, │ tokens, keys, │
│ receive- │ tree/resolve/ │ index, reconstruct,│ HTTP gateway, │ browse, usage │
│ pack, LFS │ tokens, LFS batch │ server-side chunk │ cache tier, HF mirror │ │
└─────┬─────┴─────────┬──────────┴─────────┬──────────┴───────────┬────────────┴────────────────┘
│ auth adapters │ metadata index │ object store pools │
▼ ▼ ▼ ▼
OIDC/JWT · webhook · Postgres | SQLite S3 · GCS · Azure · MinIO/R2/Garage · local disk
static · access tokens · SSH keys
- One binary,
openxet. Roles (git,ssh,hub,cas,transfer,web) are enabled by config. Default is all-in-one; large deployments split and scale roles separately. - Nodes hold no state that matters. Any node can serve any request. Scale by adding nodes behind a load balancer.
5.2 Components (crate-level seams; exact layout is the builder's call)
- protocol: hash-string conversion, xorb and shard parsing, serialization
and verification. Prefer depending on
xet-core-structuresfor the reference implementation; wrap it so upstream churn is contained in one place. Spike S5 confirmed it covers everything the server needs, with an exact version pin (ADR 0001). - cas: the CAS endpoints. Xorb ingest: stream, parse, recompute chunk
hashes and xorb hash, reject on mismatch, write to the object store, record
the chunk table. Shard ingest: verify that every referenced xorb exists and
that verification entries match stored chunk hashes, then register files,
their reconstruction terms and sha256 metadata, and eligible chunks for
global dedup. Reconstruction, batch reconstruction, global dedup query,
head endpoints,
/v2/shardsprogress streaming,/v2/file-chunk-hashes. - index (trait plus implementations): xorbs (hash, size, chunk byte offsets), files (hash, size, compact term list, sha256), dedup entries (eligible chunk → shard/xorb), scope membership (§5.4), GC metadata. Postgres for production, partitioned by hash prefix. SQLite for single node. The trait leaves room for a distributed KV later without touching callers.
- storage (adapter over the
object_storecrate or equivalent): S3 and S3-compatible stores, GCS, Azure, local filesystem. Presigned-URL generation where the backend supports it. - transfer: serves xorb byte ranges for backends without presigned URLs (local disk), for backends that cannot bind a range, and for an optional NVMe or CDN cache tier. Signed, short-lived, range-bound URLs.
- hub (optional): the Hub-compat API (§6).
- auth: authenticators, authorizers and CAS token minting (§5.4).
- gc: garbage collection (§5.6).
- server: wiring, config, roles, health, metrics, tracing.
5.3 Storage
Building our own distributed object store means taking on replication, repair and durability: years of work, and the one place a bug destroys user data. Xet already stores data as large objects (xorbs up to 64 MiB) read via range requests, so per-request object-store overhead is small. Speed comes from:
- presigned direct downloads
- an optional cache tier
- a fast index
The bucket is the source of truth. Xorbs, uploaded shards, and Hub-layer
commit objects are written to the bucket. The database is an index that
openxet reindex can rebuild from the bucket. Losing the database must never
lose data.
Durability details (builder-check calls):
- Commit objects and storage-only pins are written to the bucket synchronously, before they are acknowledged.
- Branch and tag pointers live in the database, which is authoritative while
it exists.
- Every ref change also inserts a row into the database ref log in the same transaction (ADR 0004).
- The ref log in the bucket is derived from those rows within seconds (documented RPO ≤ 60 s).
- So after total database loss,
reindexmay roll a branch back to an earlier commit, but no data or commit object is lost. Newer commits it cannot place on a branch, and annotated tags no ref reaches, are listed as recoverable, byreindexand byverify. - A pack that holds no object reachable from a rebuilt ref goes to a
recoveredstate (ADR 0004). It is never served and GC never collects it. - Unplaced commits, unplaced tags and recovered packs are persisted in
the database.
verifykeeps listing them, and repacks keep everything they need, so nothing restorable is dropped. Full repacks are never blocked. - Nothing is discarded automatically. A metric and an alert flag
unresolved repos, and an operator, after review, runs
openxet reindex restoreoropenxet reindex discard(per repo or bulk, with--dry-run). Until then the data stays stored. discardis immediate and irreversible for unplaced objects in committed packs: their mtime is their pack's creation time, so the next full repack drops every one whose pack is older than the grace period, with no further grace. Recovered packs still get GC's grace period.--dry-runand the docs say so (ADR 0004 item 4).
5.4 Identity, authorization and tenancy
openxet never stores users. A request carries a credential; an authenticator turns it into a principal (subject plus claims); an authorizer decides read or write on a repo or scope.
Authenticators (v1):
- OIDC/JWT: validate tokens from any OIDC issuer via JWKS (Keycloak, Auth0, Okta, Entra, Google, GitHub Actions OIDC and others).
- Static tokens: from config, for dev, CI and small setups.
- openxet access tokens (decided by the user): long-lived, revocable
tokens for tools like
hf auth login, each bound to an external identity.- A user proves who they are once through the operator's identity system,
for example
openxet login(OIDC device or browser flow), a minimal login page, or the platform's backend calling the admin API. - openxet then issues a token. It stores only a hash plus the bound subject, claims snapshot, scopes, expiry and last use.
- Authorization is re-evaluated against the authorizer on use, so revoking access upstream takes effect even if the token itself is still valid.
- openxet still owns no users: no passwords, profiles or sign-up.
whoami-v2answers from the bound identity, with names and orgs mapped from claims via config or the webhook.
- A user proves who they are once through the operator's identity system,
for example
- Who may create repos: decided by the authorizer, as a
createaction on the namespace.
Authorizers (v1):
- Config rules on claims and repo patterns (e.g. group
ml-teammay writeorg/*). - Webhook: openxet asks the operator's backend "may subject X read/write repo Y?", with a short-TTL cache. This is how platforms like aqora plug in.
CAS tokens are short-lived EdDSA JWTs (ADR 0007), scoped to
(scope, read|write, exp), with no revision, because Xet reads follow scope
membership (R25) and a revision would enforce nothing. The Hub token route
still accepts a revision in its URL for compatibility (M2). Every node
verifies them statelessly against a key set of public keys, so keys rotate
without downtime, and a platform can sign them itself. exp is honoured
exactly.
Storage-only mode: a platform with its own naming layer gets CAS tokens for its users either through an admin endpoint or by signing them itself with a configured key. It then stores Xet file hashes in its own database.
Scopes and tenants (builder-check call):
- A scope is the unit of access. In the Hub layer a scope is a repo
(
{type}/{namespace}/{name}), matching HF's repo-scoped Xet tokens. In storage-only mode it is an opaque string the platform chooses. - A tenant is the unit of isolation for
per-scopededup and metering. In the Hub layer it is the namespace, the first path segment; in storage-only mode it is configurable. - A commit in repo B may only reference files registered to repo B. A file already in repo A is registered to B by uploading it to B; dedup makes that nearly free, because only a shard is sent.
File access scoping: files registered by a shard upload are associated with the token's scope, and so is the sha256 → Xet hash mapping from the shard (never global, since the sha256 is client-asserted). Reconstruction and head-file only succeed for files registered to the caller's scope. Xorbs remain globally content-addressed and shared.
Xet reads by hash follow scope membership, not Git reachability (ruling R25):
- Readers of a scope may read any file registered to it until GC collects it. That includes a file that only force-pushed-away commits referenced, although Git fetches and Hub reads by revision refuse those commits (R22, ADR 0004).
- Why: a Xet hash is an unguessable 256-bit content hash. A reader can only have it from history they could see while it was current.
- The trade-off is for users: reads stay stable, so in-flight downloads and caches don't break when a branch moves.
- Security implication: force-pushing a secret out of history does not
revoke access to it. Anyone in the scope who saw the hash can still
download the file. Removing a secret takes a force-push plus GC, or
openxet purge <file-hash>(M2+). Purge unregisters the file from every scope at once and logs it. New reconstructions stop immediately; already-issued URLs expire within their lifetime (at most 60 min, ADR 0002, ruling M1-R42). Purge also evicts the file's ranges from the cache tier. The bytes then leave storage through GC (§5.6), once no other file references their xorbs. Removing a reader from the scope revokes their access once their current CAS token expires.
Global-dedup visibility is configurable:
global, the default, follows HF's model with HMAC-protected responses.per-scopeis for multi-tenant hosting, where revealing whether a chunk already exists across tenants is unacceptable.
A dedup answer offers every xorb of the newest visible shard that
references the matched xorb, up to 16 MiB, so one hit lets a client with an
empty cache dedup a whole earlier upload (rulings M1-R30, M1-R32, S11); under
per-scope that shard must be registered to the caller's scope and only
the caller's xorbs are offered.
Existence checks in per-scope mode answer per scope too, or they
would reveal what dedup answers hide. Every check that says whether
something exists is filtered by the caller's scope:
HEAD /v1/xorbs/…,HEAD /v1/files/…and dedup lookups (GET /v1/chunks/…)- the xorb lookups of shard verification (
verify_shard'sXorbChunkLookup)
A xorb counts as registered to a scope once it was uploaded under that
scope or a file registered to the scope references it. A xorb or file not
registered to the caller's scope answers exactly as if it were absent
(404, no dedup match, UnknownXorb), so the client uploads it, which
registers it to its scope. Storage stays shared: the upload of a xorb
already stored writes nothing new. Test:
per_scope_existence_checks_hide_other_tenants.
5.5 Data paths
-
Upload: client → CAS (
POST xorbsstreamed and verified into the object store;POST shardsverified and registered). This scales with node count. Today every xorb byte goes through openxet (§3).- Xorb upload grants are a tracked future item, not an ADR yet, since
no stable client uses them (§3). When clients ship them, openxet
supports them with verification before registration: the grant
URL is a presigned PUT to a staging key, bound to the declared length
and CRC64-NVMe checksum where the store can enforce them. The commit
call reads the staged object back, recomputes the chunk hashes and
xorb hash exactly as
POST /v1/xorbsdoes, and only then moves it to its content-addressed key and records its chunk table. A mismatch deletes the staged object and fails the commit. Unverified bytes are never readable and never count for dedup or shard verification. Staging keys expire with their grant, and GC sweeps the ones left behind. Grant URLs allow CORS for browser clients. - Until then
/v1/xorb-grants/*answers404, so grant-aware clients fall back toPOST /v1/xorbs.
- Xorb upload grants are a tracked future item, not an ADR yet, since
no stable client uses them (§3). When clients ship them, openxet
supports them with verification before registration: the grant
URL is a presigned PUT to a staging key, bound to the declared length
and CRC64-NVMe checksum where the store can enforce them. The commit
call reads the staged object back, recomputes the chunk hashes and
xorb hash exactly as
-
Download: client → CAS reconstruction (small JSON). Then either:
- (a) direct from the object store via native presigned URLs, one range-bound URL per fetch entry (S1, ADR 0002); or
- (b) via the transfer role, with openxet-signed range URLs (also one
range per entry, so no
multipart/byteranges), for local disk, caching setups and any pool that fails the range-binding probe. The URL form is{public_url}/v1/transfer/xorbs/{xorb}?range={start}-{end}&exp={unix}&sig={b64url}, wheresigis an HMAC-SHA256 over the xorb, range and expiry, keyed with the transfer key. The request must carry exactly the signedRange: a missing header is a400, and a different range, an expired URL or a bad signature is a403.
Mode (a) is gated per deployment and pool by a range-binding enforcement probe, not by a backend list (ADR 0002). It runs at startup and in
openxet doctor/openxet config check. openxet presigns a GET for one range of a probe object and requires all three: the signed range returns206, a different range returns403, and noRangeheader returns a4xx. Only then does the pool serve direct URLs; otherwise it falls back to (b) and logs why. This applies to every backend (S3, R2, Garage, MinIO, and GCS or Azure once their signers can bind a range). CI runs the probe against MinIO. URL lifetime defaults to 60 min, with a 30 min minimum as derived in ADR 0002 and a 60 min maximum (ruling M1-R42). v1 reconstruction is served too, for older clients. -
Plain HTTP read gateway:
GET /{repo}/resolve/{rev}/{path}, when the redirect is followed, streams the reconstructed file with HTTPRangesupport. This makes openxet readable by anything that speaks HTTP (curl, browsers, pandas, polars, DuckDB via the plain resolve URL) orhf://through fsspecHfFileSystem. DuckDB and polars hard-code huggingface.co in their ownhf://handlers (spike S4,docs/research/S4-analytics-clients.md), so they read openxet throughhttps://{host}/{repo_type}/{repo}/resolve/{rev}/{path}. The gateway must pass the conformance checklist in that note (HEAD, all threeRangeforms,416,ETag/If-Range, redirects, auth); private repos need a DuckDBhttpsecret, and polars needs an fsspec file object. Private-CA https works in DuckDB but not in polars on macOS (observed;docs/research/S4-analytics-clients.md). It is served by the transfer role so it scales and caches independently.
5.6 Garbage collection and retention
- References: Hub-layer commits reference files; in storage-only mode the platform pins files through an admin API, optionally with a TTL. Files reference xorbs.
- Mark and sweep with a grace period: default 30 days, minimum 28
(21-day client shard cache + 7-day dedup key), and
gcrejects anything lower. Stock clients dedup against cached shards without asking the server (§3), so a xorb collected earlier fails their next upload. The grace also covers in-flight uploads, because xorbs land before the shard that references them. - Storage-only default is safe: files are never collected unless the operator enables pin-based retention for that tenant. An integrator who never pins loses nothing. (builder-check call)
openxet gc --dry-runreports what would be deleted. Every deletion is logged. Branch and commit deletion in the Hub layer only drops references; data goes only via GC.- Git pack side files leave with what they describe (ADR 0004 item 5):
a pack's
.idx,.rev,.bitmapand.mtimesare deleted together with the pack, and amulti-pack-indexgeneration (with its.bitmapand.rev) once a newer generation is committed and the grace period has passed. Neither is ever left behind as an orphan.
5.7 Metering
Logical bytes (what users uploaded) and physical bytes (what dedup actually stored) are tracked per scope and per tenant. They are exposed via the admin API and metrics, because platforms need them for quotas and billing. (builder-check call)
5.8 Scale targets (initial; validated by benchmarks, revised with data)
-
Database topology for v1 (builder-check call): openxet nodes scale out; Postgres scales up. That means one primary, read replicas for reconstruction and listing reads, and native partitioning by hash prefix. The index trait does not assume a single instance, so a sharded backend (Citus, or a distributed KV) can come later without touching callers.
-
Write rate target: 2,000 xorb registrations/s and 200 shard registrations/s per deployment.
-
Index sizing at 1 PB of unique data: about 1.6×10¹⁰ chunks, about 1.6×10⁷ xorbs and about 1.6×10⁷ dedup-eligible entries plus first-chunks. That fits partitioned Postgres; the chunk-to-xorb mapping lives in per-xorb chunk tables, not per-chunk rows.
-
Server-side metadata latency: reconstruction p99 < 50 ms for files up to 100k terms; token, head and resolve p99 < 10 ms (excluding the authorizer webhook).
-
Ingest: a single node sustains ≥ 9 Gbit/s of xorb upload; throughput scales roughly linearly with nodes.
-
Time to first upload: under 2 minutes from a clean machine, measured on every release.
5.9 Security requirements
Each requirement is a release blocker and has a named test. The tests are planned for the milestone that builds the surface (M1 for CAS, M2 for Hub and Git, later for caches and the mirror); a surface doesn't ship without its test. Security tests maps each test to its file and milestone, and CI keeps that map and this table in step.
| # | Requirement | Tests |
|---|---|---|
| 1 | No unauthenticated reads when auth is on. Every data URL (reconstruction, xorb ranges, the HTTP gateway, LFS hrefs) needs a credential, or is a signed URL bound to one range and a short lifetime (ADR 0002). | no_unauthenticated_reads walks every route and every URL openxet emits without a credential; signed_url_bound_to_range |
| 2 | Credentials are repo-scoped. CAS tokens and capability hrefs are scoped to one repo; signed URLs are bound to one xorb range and an expiry (ADR 0002) and are issued only after the caller's repo check; user credentials are authorized per repo on every Hub and CAS route. A foreign repo answers 403, or 404 where the caller can't read it, never revealing a private repo's existence. Repo creation is authorized per namespace (§5.4). | credential_scoped_to_repo, a matrix of every route × read, write and create with another repo's credential; repo_create_authorized_per_namespace |
| 3 | Caches and proxies are keyed by the caller's credential for anything that depends on authorization. They never share tokens or authorized responses, and they fail closed. The only exception is stale serving of content whose last upstream check was public and ungated (§10.6). | cache_keyed_by_caller, cache_fails_closed |
| 4 | Dedup answers reveal no raw chunk hashes. They are HMAC-keyed shards with a footer, and per-tenant dedup (per-scope, §5.4) is available. In per-scope mode every existence check (HEAD of xorbs and files, dedup lookups, shard xorb lookups) answers as absent for anything not registered to the caller's scope. | dedup_response_hmac_keyed, dedup_answer_never_contains_raw_hashes_or_file_info, per_scope_dedup_isolates_tenants, per_scope_multi_xorb_answer_hides_foreign_xorbs, per_scope_existence_checks_hide_other_tenants |
| 5 | Uploads are verified. Xorbs are re-hashed on ingest. Shards are checked against the stored xorb chunk hashes by verify_shard, with its work cap (S6), and a shard that references a missing xorb is rejected. Verification errors reveal no value derived from stored data, so they can't stand in for holding the chunks (M1-R23). | the S6 rule tests; shard_with_missing_xorb_rejected, shard_verify_work_capped, verification_errors_reveal_no_server_derived_values, foreign_writer_cannot_forge_shard_under_global |
| 6 | Credentials are never echoed or logged. LFS download actions carry no CAS token (ADR 0003), no action header or response carries the client's own credential, and fixtures and docs pass the redaction guard. | lfs_download_action_has_no_token, responses_never_echo_credential, credentials_never_logged (scans all logs of a suite run), test_fixture_redaction |
| 7 | No silent no-op success. Every acknowledged write has its effect, and survives a SIGKILL right after the acknowledgement. | acknowledged_xorb_write_is_visible, acknowledged_shard_write_is_visible, acked_write_survives_sigkill |
| 8 | Git reads check reachability on every path (R22), and every ref writer is fenced (ADR 0004). | ADR 0004's v2_want_unreachable_refused, hub_read_by_sha_obeys_r22, writer_cannot_make_unreachable_reachable, hub_ref_write_fenced, ref_log_in_cas_transaction |
Xet reads by hash follow scope membership (R25, §5.4); that is by design, not a gap in requirement 8.
6. Hub-compatibility layer (v1 surface)
Only what huggingface_hub / hf need for the workflows below. Exact
request and response shapes come from huggingface_hub source and recorded
fixtures, pinned by the compatibility suite. Spike S3 captured them for
huggingface_hub 2.2.0 (compat/fixtures/hub/,
S3).
Workflows:
hf auth login- create and delete repo
hf upload/upload_file/upload_folder/create_commit, including deleteshf download/hf_hub_download/snapshot_downloadlist_repo_files/list_repo_tree/get_paths_info- repo info
- refs and commits listing
- create and delete branch and tag
Endpoints (verified to be used by huggingface_hub):
GET /api/whoami-v2POST /api/repos/create(a409carriesurl, whichcreate_repo(exist_ok=True)reads),DELETE /api/repos/deletewith a JSON bodyGET /api/{type}s/{id}[/revision/{rev}]GET /api/{type}s/{id}/refs,GET /api/{type}s/{id}/commits/{rev}GET /api/{type}s/{id}/tree/{rev}/{path},POST /api/{type}s/{id}/paths-info/{rev}POST /api/{type}s/{id}/preupload/{rev}GET /api/{type}s/{id}/xet-{read|write}-token/{rev}, returning the token in both the JSON body and theX-Xet-*headers (§3)POST /api/validate-yaml: every commit that adds aREADME.mdposts it here first, and any non-2xx fails the upload. It answers200 {"errors":[],"warnings":[]}or real validation, never501.POST /api/{type}s/{id}/commit/{rev}(NDJSON)- branch and tag create/delete
HEAD|GET /{[datasets/]}{id}/resolve/{rev}/{path}
Model:
- Repo types
model,dataset,space,kernelandbucket(§10.4). - Every commit is a real Git commit, just as on huggingface.co. Small
files are Git blobs. Large files are Git LFS pointer files whose sha256 maps
to a Xet file hash. Commits made through the Hub API (
hf upload) and throughgit pushare the same objects, so either can be cloned, pulled or downloaded by the other (§10.9). - Refs are branches and tags with compare-and-swap updates, so concurrent commits are safe.
- The Hub API obeys the Git engine's safety rules (ADR 0004 item 6), so
it can't be used to get around them:
- Every Hub read and write runs on the same per-request snapshot of committed packs as a Git request.
- A revision given as a commit OID must be reachable from the current refs, under the same rule that Git fetches use (R22).
- Hub ref creates, updates and deletes and Hub commits use the push's fenced commit transaction. Hub commits get a connectivity check against the snapshot first, and a branch or tag can only be created at an object the current refs already reach.
Small files: preupload decides whether small "regular" files go inline
in the commit (stored as content-addressed blobs) or through Xet. The
threshold is the builder's call, informed by benchmarks. Answering
"uploadMode": "lfs" is the server's only lever; hf_xet then takes the
Xet path (S3). The compatibility test for this (competitive-matrix lesson
- uploads a file of at least 1 GiB whose name matches no
.gitattributespattern, so it proves openxet routes by size, not by pattern.
Explicitly not implemented: model cards, search, likes, discussions,
Spaces, inference, collections, org settings. Unimplemented endpoints
return a clear 501 that names the endpoint, never a confusing 404. The
exception is a route where stock clients fall back only on 404, such as
/v1/xorb-grants/* (§3).
6.1 Git LFS endpoint for git-xet (decided by the user: core, v1)
The point is Xet dedup for any Git repo, wherever it is hosted. A repo on
GitHub, GitLab, Gitea or Bitbucket sets
git config lfs.url https://openxet.example/{namespace}/{repo}.git/info/lfs
(or commits it in .lfsconfig), and git push / git pull move large
files through openxet with chunk-level dedup.
How it works (verified in xet-core/git_xet source, 2026-10-23):
git-lfssends the Git LFS Batch API request with its registered transfer agents.- openxet answers uploads with transfer
xet. Each object'sactioncarrieshref(the token refresh route) and headersX-Xet-Cas-Url,X-Xet-Access-Token,X-Xet-Token-Expirationand optionallyX-Xet-Session-Id. huggingface.co does exactly this, and itshrefis the Hub token route…/xet-write-token/{url-encoded ref}(S7, observed). git-lfshands off togit-xet, which talks to the CAS directly.
Server work is small because it reuses pieces v1 needs anyway:
- the Batch API endpoint
- the sha256 → Xet file hash mapping (LFS pointers are sha256 oids)
- the token route
Rules for this endpoint:
- Auth uses Git's normal credential flow: an openxet access token as the HTTP password.
- LFS repos are scopes like Hub repos. Creating one is an authorizer
createdecision, so the first push can create it. - Downloads always answer
basic.git-xet0.2.1 implements uploads only ("custom transfer for download is not implemented yet"). Butgit xet installregisters it for both directions, sogit-lfsoffersxetin download batches. A server that choosesxetthere breaksgit lfs pullwith no fallback (S7, observed). openxet therefore answers every download withbasic, as huggingface.co does:- absolute
hrefs, never relative, served by the HTTP read gateway (§5.5) - only
X-Xet-Hashin the action'sheader, which is not a secret. Xet download agents get the CAS token fromGET {lfs-url}/xet-read-token, so it never appears in batch responses orGIT_TRACEoutput. - No action
headeror response ever carries the client's own credential (§5.9 req 6). (ADR 0003)
- absolute
- Closing the Git download gap (decided by the user, v1). With plain
basic,git clone/git pullfetch whole files. Two complementary fixes:- Our client is a Git LFS transfer agent for downloads (§10.8).
openxet setup gitregisters it under its own transfer name (notxet, whichgit-xetowns and can't download with), so Git fetches only the chunks missing from the local Xet cache. The cache is shared withhfandhf_xet(sameHF_XET_CACHE). Uploads keep going throughgit-xetwhen it is installed, or through our agent when it isn't. - Upstream: contribute download support to
git-xetinhuggingface/xet-core, so every Git user benefits, not just ours. The credential-sourcing change (§10.2) goes in the same effort.
- Our client is a Git LFS transfer agent for downloads (§10.8).
- Clients without
git-xet:- Downloads work the same way. Collaborators who only clone don't need anything extra.
- Uploads work via
basic/multipart, with server-side chunking (§10.3), so the data is still deduplicated in storage. The response suggests installinggit-xetfor faster uploads.
- There is no
git-xetdownload handshake to match.git-xet0.2.1 doesn't download, and huggingface.co'sbasicdownload actions carry noX-Xet-Hash(S7,docs/research/S7-git-lfs-xet.md). The handshake for our agent and an upstreamgit-xetis ours to define: the hash in thebasicaction'sheader, per ADR 0003.
Xet-native clients (chunk first, oids later) use the LFS-scoped token
routes GET {lfs-url}/xet-read-token and GET {lfs-url}/xet-write-token.
They return the Hub token shape {accessToken, exp, casUrl}, and tokens are
scoped to (scope, read|write), never to objects. A shard upload registers
each file's xet hash and its sha256 to the scope, and a read token allows
reconstruction of any file registered to it. Pointers are always the
canonical 3-key form. The full contract is
ADR 0005.
7. Operability and user experience
- Zero-config dev mode:
openxet serveruns with SQLite and a local directory, prints a static token and the exactexport HF_ENDPOINT=… HF_TOKEN=…lines, and works immediately. - Config: one TOML file, with every key overridable as
OPENXET_*.openxet config checkvalidates and explains. - Errors are actionable: they say what happened and what to do next.
- Observability:
- Prometheus
/metricsand OpenTelemetry traces. - Structured logs. Tokens are never logged.
/healthz,/readyz.
- Prometheus
- Admin CLI:
reindex,gc [--dry-run],verify(integrity scan),token mint,migrate,purge <file-hash>(§5.4, M2+). - Feedback loop:
- GitHub Discussions, issue templates and a public roadmap.
- A docs feedback link.
- No telemetry, by default or ever without explicit opt-in. Client
telemetry sent to
POST /v1/telemetryis accepted and dropped unread (§3).
7.1 Humans and agents as first-class users (decided by the user)
openxet is built for people and AI agents, and for the two working together. Each gets the best possible experience, and neither is a degraded mode of the other.
For humans:
- Readable output, progress bars and helpful prompts. Safe defaults with confirmation before destructive actions.
- The web UI, and docs that teach.
For agents:
- Every surface is machine-usable:
- Every CLI command has
--jsonoutput with a documented, versioned schema. - Exit codes are stable and documented.
--yes/ non-interactive mode never blocks on a prompt.- Admin, Hub and CAS APIs are described by a published OpenAPI document that is generated from code and checked in CI.
- Every CLI command has
- Errors are structured: a stable code, a message, and a
hintwith the next action, in both CLI JSON and HTTP responses. An agent can recover without parsing prose. - Predictable and safe to retry:
- Commands and API writes are idempotent where possible, with idempotency keys where not.
--dry-runon everything that changes state.- Explicit, deterministic pagination.
- An MCP server (
openxet mcp, in the client) exposes repos, files, upload, download, search over paths, usage and admin operations as tools with tight schemas. It respects the caller's token scope. - Agent-readable docs:
llms.txtand per-page plain Markdown on the docs site. Every CLI command's help matches the docs, and both are generated from one source.
For humans and agents together:
-
Delegated, attenuated tokens. A human mints a token for an agent that is narrower than their own:
- specific repos
- read-only or write
- expiry measured in hours
- optional limits on bytes or operations
It is revocable from the web UI.
-
Every action is attributed in the audit log as human, or as agent on behalf of a human. The web UI shows what an agent did.
-
Approval gates: operators can require human approval for agent-initiated destructive or costly actions (deleting a repo or branch, force-moving refs, GC runs, large egress). The agent gets a pending state it can poll; the human approves in the UI or CLI.
-
Handoff: any CLI or UI action can produce the equivalent command or API call, so a human can hand a task to an agent and back.
Quality bar: the compatibility suite has an "agent leg." It runs scripted,
non-interactive workflows using only --json, exit codes and the MCP
server, and fails on any prompt, unparseable output or unstable schema.
8. Engineering practice
Language and tooling:
- Rust stable with a stated MSRV; tokio and axum, or equivalents chosen in planning with reasons.
#![forbid(unsafe_code)]unless an ADR justifies an exception.- rustfmt, clippy with warnings denied in CI,
cargo-denyfor licenses and advisories,cargo-nextest.
Tests, each layer earning its place:
-
Unit and property tests (proptest) on hashing, hash-string conversion, xorb, shard and term math.
-
Golden vectors from
xet-team/xet-spec-reference-files, plus vectors generated byxet-core. -
Fuzzing (
cargo-fuzz) of every parser that reads untrusted bytes (xorb, shard, NDJSON commit). The xorb and shard targets (read_upload_shardand the xorb parsers) are an M1 task. -
Compatibility suite, the most important test. CI runs real
hf_xet,huggingface_hub/hfCLI,huggingface.jsandgit+git-lfs+git-xetwith a Gitea remote (pinned and latest) against openxet, across a matrix:- SQLite with local disk
- Postgres with MinIO
It covers upload, re-upload dedup, range download, branch workflows and token refresh. A "third-party Xet client" leg replays the expectations of jj-xet's
tests/openxet.rsmock against openxet (ADR 0005). A nightly job against latest upstream catches protocol drift early. -
Fault injection: object store errors, DB failover, killed nodes mid-upload.
-
Benchmarks with regression thresholds in CI; published numbers in docs.
Decisions and history:
- Architecture Decision Records live in
docs/adr/. - Conventional Commits with a scope per component, so
git logcan be read per work package. - Before changing a component, read its ADRs and
git log -- <path>(this rule goes intoAGENTS.md).
AGENTS.md, written first and kept current:
- architecture map
- exact commands
- invariants (protocol exactness, bucket is truth, no user ownership, never delete outside GC)
- definition of done (tests, docs, ADR if a decision changed)
- history-check rule
- anti-slop rules: no speculative abstraction, no narrating comments, no dead code, no unverified claims in docs
- how to verify against upstream
Docs live in the repo and are built into a docs site. Examples are run in CI so they cannot rot. Docs cover: quickstart, operator guide per backend, auth recipes (Keycloak, Auth0, GitHub OIDC, webhook), storage-only integration guide for platforms, upgrade notes, and the compatibility matrix.
Release: SemVer and a changelog. The protocol compatibility statement says which spec version and client versions are supported.
9. Distribution
- v1:
- static binaries for linux/macOS on amd64 and arm64, via GitHub Releases
(cargo-dist or equivalent), a Homebrew tap and
cargo install - multi-arch container images on GHCR
- a Helm chart
docker composequickstart with MinIO. Per the S2 outcomes (docs/research/S1-S2-direct-downloads.md):http://on loopback by default- TLS docs per platform: on macOS
hf_xetignoresSSL_CERT_FILE, so a private CA must be trusted in the system store - openxet presigns with a store endpoint the client can reach, not the compose-internal service name
- Terraform examples for AWS, GCP and Azure
- library crates on crates.io for embedding the storage core
- The
gitrole needsgit≥ 2.37 onPATH(ADR 0004).- openxet runs git as a separate program. That is aggregation, so openxet stays Apache-2.0 (inferred, not legal advice).
- Container images ship the distro's unmodified git package, which is GPL-2.0, and list it in their licence notice.
- Each image release publishes the exact git source package next to the image, or includes a written offer for it (GPL-2.0 §3).
- The static binaries don't bundle git. They check for it at start-up
when the
gitorsshrole is enabled, and fail with an actionable error.
- static binaries for linux/macOS on amd64 and arm64, via GitHub Releases
(cargo-dist or equivalent), a Homebrew tap and
- Also v1:
- one-click templates (Railway, Fly, Render)
- a Nix package
- cloud marketplace listings (AWS, GCP, Azure). These need publisher accounts, which is a non-code task for the user.
- A Kubernetes operator, for multi-role deployments, key rotation and GC scheduling. It is built only if Helm proves insufficient for these; the plan states which way it went and why.
- Discoverability:
- listing in awesome-selfhosted
- a "self-host Xet" guide that ranks for the obvious queries
- published benchmarks
- a clear "works with the official HF tools" message
10. Further v1 scope (decided by the user: ambitious, nothing deferred)
10.1 Git over SSH
- openxet runs an SSH endpoint, enabled by the
sshrole. Identities are SSH public keys bound to an external identity, the same way access tokens are (§5.4). Keys are registered viaopenxet loginor the admin API. No passwords and no users are stored. git-lfs-authenticate: returns{href, header.Authorization, expires_in}, all three always. The LFS spec makes them optional, butgit-xet0.2.1 fails withouthreforexpires_in(S7: Gitea omitsexpires_inandgit-xetbreaks against it).git-lfsuses it when the LFS endpoint is SSH.git-xetuses it when the Git remote is SSH, on every token refresh.git-lfs-transfer(the pure-SSH LFS protocol, git-lfs ≥ 3.0): lets plaingit-lfsupload and download over SSH with no HTTPS at all. Uploads go through server-side chunking (§10.3).- It can't negotiate
xet: the client always sendstransfer=sshand uses its built-insshadapter (S7, source and observed). git-lfsprefers it whenever the LFS endpoint is SSH. So for openxet-hosted repos over SSH,openxet setup gitsetslfs.<url>.sshtransfer=neverwhengit-xetis installed, which sends those users throughgit-lfs-authenticateplus the HTTPS batch, wherexetis offered (ADR 0003).
- It can't negotiate
- Git repos whose remote is on another host over SSH (e.g.
git@github.com:…) work as long aslfs.urlpoints at openxet andopenxet setup githas written theaccesskey (§10.2).
10.2 git-xet with third-party Git hosts
When lfs.url points at openxet and the remote lives elsewhere, openxet's
policy is: capability routes never authenticate by header, and openxet setup git configures access explicitly
(ADR 0003, S7). All three
parts are v1:
-
Server side:
- The
hrefrefresh route openxet returns is a short-lived, signed, single-repo capability URL, so the refresh needs no credential. - CAS tokens issued through the LFS path last for the transfer session.
- openxet never authenticates by, logs or echoes an
Authorizationheader on that route.
- The
-
Client setup:
openxet setup gitwrites the clone's git config:lfs.urllfs.<endpoint derived from the remote>.access=none, sogit-xetcalls the capabilityhrefwithout a credential; thehrefalone completes the refresh (S7, observed)- the openxet token in the credential helper under the openxet host, for
git-lfs's batch
The
accesskey must be in git config, becausegit-xetdoesn't read.lfsconfig. -
Upstream: contribute a lazy credential lookup, an optional
expires_inand download support togit-xetinhuggingface/xet-core, coordinated with Hugging Face.
10.3 Server-side chunking for non-Xet clients
Plain git-lfs (basic transfer, HTTPS or SSH), older huggingface_hub
without hf_xet, and any LFS client can upload. openxet chunks,
deduplicates and writes xorbs and shards on ingest, using the same xet-core
chunking, so the hashes are identical to client-side uploads.
- Users get dedup in storage even if their client never heard of Xet. They don't get the bandwidth savings; the docs say so.
- Ingest scales with the
casrole. CPU cost is benchmarked. preuploadand the LFS batch endpoint offerbasicandmultipartalongsidexet.
10.4 More repo types
model, dataset, space (files only; nothing is run) and kernel
behave the same as Hub repos.
bucket (mutable, non-versioned, mirroring HF Storage Buckets): paths map
to Xet files with last-writer-wins and optional object versioning. Bucket
tokens are not revision-scoped, matching huggingface_hub.
10.5 Cache tier
- The
transferrole can run as a regional read-through cache: local NVMe, LRU over xorb ranges, and signed-URL compatible. - Reconstruction responses point clients at the nearest cache, configured by region or header.
10.6 Pull-through mirror of huggingface.co
- Self-hosted deployments can mirror public, ungated HF repos. Gated and private repos are served only to callers whose own HF credential passes an upstream check. The first download fetches from HF; later ones are served locally.
- Xet data is stored natively, so mirrored content dedups with local data.
- Air-gapped teams can pre-seed a mirror and carry it across.
Decided (R19); evidence in docs/research/S8-hf-mirror.md; ingest
mechanics to be recorded in ADR 0006:
-
Ingest verified chunks; never relay HF's xorbs or URLs. huggingface.co signs only the ranges a file needs, and files often use part of a shared xorb. Its xorb URLs expire 3600 s after issue, are range-contained, and carry
xipand, for private repos, the token owner'suser_id. Read tokens live 900 s and are scoped per repo (observed). So the mirror:- fetches term ranges promptly with a per-repo upstream token;
- decompresses and hashes every chunk;
- checks the file hash against
X-Xet-Hashand sha256 againstX-Linked-ETag; - re-packs the verified chunks into local xorbs with a shard registered to the mirrored repo's scope.
Re-packing keeps the file hash, which depends only on chunk hashes and sizes. Clients only ever see openxet URLs and tokens.
-
No unverified chunks. Until the window-merge proof test passes, a file's shard is registered only after the whole-file
X-Xet-Hashcheck. Chunks fetched earlier stay staged and unreferenced. -
Ingest goes through normal acceptance. Re-packed xorbs and the generated shard go through the normal upload acceptance:
- xorb hash recomputation;
- the
verify_shardS6 rules, including verification entries; - the 64 MiB body limit;
- the
MAX_REFERENCED_CHUNKScap, splitting shards as needed.
Ingest has no trusted bypass.
-
Licence files are kept. The mirror serves every file of a mirrored revision, including
LICENSE,NOTICEand the README, and never filters them out oftreeorresolve. -
Authorise per caller.
-
Public, ungated repos: the openxet authorizer.
-
Gated and private repos: also an upstream check with the caller's own HF credential, failing closed. That credential (for example
X-HF-Upstream-Token):- is never persisted or logged;
- is used only for that caller's check.
The check cache is keyed by (caller, repo, revision) and holds only yes or no, with a TTL.
Gated and private content is never fetched for or served to one caller using another caller's or the operator's HF credential. There is no override (R19).
-
-
Freshness and removal.
- Refs are re-resolved upstream on a short TTL; data keyed by commit is immutable.
- While upstream is down, stale serving covers only content whose last successful upstream check was public and ungated. A later gated, private or 404 answer hides it.
- Repos HF removes are hidden, and their data is reclaimed only by GC.
-
Mirrored namespaces are read-only.
-
Seeding.
openxet mirror pullpre-seeds through the same ingest path. Mirrored commit metadata lives in the bucket, so moving bucket objects plusreindexcarries a mirror across an air gap. -
The mirror is disabled on openxet.com, for every tenant, including their own private repos, until counsel review and ideally a written agreement with HF. HF's licence to users covers "through our Services", per-model licences put duties on distributors, and the Content Policy names proxies.
10.7 Multiple storage pools and data residency
- One deployment can hold several storage pools (buckets or regions). Pools are assigned per tenant or per repo by policy.
- Dedup is per pool; data never crosses pools.
- Optional per-pool encryption keys via the cloud KMS (SSE-KMS or equivalent).
- GC,
reindexand metering are pool-aware.
10.8 Our own client (decided by the user: v1, 100% compatible)
openxet ships a client: a CLI and a Rust library, with Python bindings.
It is better to use than the stock tools and never incompatible with them.
The compatibility contract is non-negotiable:
- Byte-identical chunking, hashes, xorbs and shards. The client builds on
xet-core'sxet-data/xet-core-structuresrather than reimplementing them. Any divergence is a release-blocking bug. - It works against huggingface.co itself as well as openxet, via the same
Hub and CAS APIs. Data uploaded by our client is readable by
hf,hf_xet,git-xetandhuggingface.js, and vice versa. - It reads and writes the standard HF cache layout (
HF_HOME,HF_XET_CACHE) and honoursHF_ENDPOINT/HF_TOKEN, so it coexists withhfon the same machine. - CI proves it in both directions: our client against openxet and against
huggingface.co (nightly, using a dedicated test org), plus cross-tool round
trips (upload with ours, download with
hf, and the reverse).
What makes it worth installing (the extra mile for users):
sync: rsync-style, two-way sync of a directory with a repo or bucket. Only changed chunks move.mount: read-only FUSE (Linux) / FSKit or macFUSE (macOS) mount of any repo revision, with lazy chunk fetch. Train directly off a mounted dataset.- Storage-only mode: upload and download by Xet file hash with no Hub layer, for platforms.
- Resumable everything: interrupted uploads and downloads resume, and
progress output is human and
--json. - Chunk-cache seeding:
openxetcan seed the local Xet chunk cache from an existing Git LFS checkout, so the firstgit pullor download after switching to openxet fetches only changed chunks (§1a). login: the OIDC device/browser flow that issues openxet access tokens and SSH key registration (§5.4, §10.1).setup git: one command configureslfs.url, credentials andgit-xetfor a repo (§10.2). It also registersopenxetas the Git LFS transfer agent, sogit clone/git pulldownload only missing chunks (§6.1). It works against openxet and huggingface.co alike.- Diagnostics:
doctorchecks connectivity, auth, clock skew and versions, then says what to fix.
Distribution: the same channels as the server (§9), plus PyPI (Python
bindings), npm via WASM if the bindings allow it, winget, and Windows
binaries. The client must run on Windows; the server need not.
10.9 Git hosting (decided by the user: v1)
git clone https://openxet.example/org/model (and git@openxet.example:org/model)
works as it does on huggingface.co. This sits alongside §6.1, where the repo
lives elsewhere and openxet is only its LFS store; both modes are supported.
Behaviour:
- Protocols: Git smart HTTP and SSH, both
upload-packandreceive-pack, protocol v2 where clients support it. The LFS endpoint is implicit at{repo}.git/info/lfs, sogit-xetandgit-lfswork with no configuration. §10.2 doesn't apply, because the Git remote is openxet. - Large files land in Xet automatically:
- Pushes with
git-xettake the Xet path. - Plain
git-lfspushes go through server-side chunking (§10.3). - Large blobs pushed without LFS are kept as ordinary Git objects. They
don't silently corrupt history. openxet reports them and offers
openxet migrate-lfs. Optionally, the operator can reject them above a size limit, with a clear message.
- Pushes with
- One history: Hub API commits and Git pushes write into the same Git history under the same ref compare-and-swap rules.
- Stateless nodes still hold: Git objects live in the bucket (packfiles plus index) and refs live in the database, with the §5.3 durability rules. Nodes may keep a local read cache of packs.
- Hooks for integrators: post-receive webhooks (repo, ref, old/new sha) so platforms can react to pushes, e.g. to trigger CI.
Engine (decided by S9, ADR 0004):
- The
gitbinary serves the wire protocol.-
Each request runs
git upload-pack/receive-packon a throwaway snapshot repo, built from one database snapshot:packed-refs- hard links to exactly the committed packs in the node's cache, with no alternates
-
The snapshot lives on the cache's filesystem, which start-up checks.
-
openxet answers the protocol v2 capability advertisement and
ls-refsin-process. -
openxet enforces reachability for every upload-pack want, v0 to v2 (R22), because git's v2
upload-packserves any object in the odb. It interposes on the request stream and checks wants before forwarding them to git.- Ref tips pass.
- All other wants share one gix walk per request, within a configured commit, time and concurrency budget.
- Anything not found is refused (fail closed).
-
Objects are never shared across repos.
-
Ref updates go through a hardened
proc-receivehook back into openxet:- an inherited socketpair fd (HTTP and SSH) and a single-use token bound to repo, principal and request
- pack paths confined to the request's snapshot
- policy re-checked in openxet
- a sanitised git environment
-
openxet then publishes the pushed pack to the bucket as
pending. Next it runs one transaction that does four things:- the ref CAS
- marks the pack
committed, only while it is stillpending - checks
full_repack_gen - inserts the database
ref_logrows
Losing pushes mark their pack
lost(again only while it ispending). Then it acknowledges. -
The
ref_logtable is written by every ref writer, Hub API included, in its CAS transaction. The bucket ref log (§5.3) is derived from it. -
Every ref writer uses that same transaction,
full_repack_gencheck included: pushes, Hub ref creates, updates and deletes, and Hub commits. Any failure, the fence included, leaves the writer's packlost. -
A writer may only make reachable what was already reachable from its snapshot's refs, or what the client itself sent. Bases that git's
--fix-thinappends don't count as sent, so every appended base must be reachable from the snapshot's refs. Path-guided lookups in the trees of boundary commits and ref tips settle most pushes; the rest take a full walk under its own budget, failing closed. That stops a push or a Hubcreate_branchfrom resurrecting, or copying out, a force-pushed-away object. Until thegit-reachabilitybitmap follow-up, some honest thin pushes can be refused (ADR 0004 item 3); the refusal hintsgit push --no-thin, which sends no thin bases. -
The v2 interposer allows only
ls-refsandfetch. git 2.37–2.44 servesobject-infofor any object, so openxet refuses it, along withbundle-uriand unknown commands. -
Each request reads refs and its pack set in one database snapshot, from the primary.
- Only
committedpacks are exposed to new snapshots. pending,lost,supersededandrecoveredpacks never are, so a push's connectivity check can't lean on another in-flight pack or on a repack input.- In-flight requests keep their links.
- Only
-
- Repacking takes committed packs only, on one leaseholder per repo:
- geometric, with a multi-pack-index bitmap, which leaves cruft packs alone
- plus a periodic full
repack -adb --cruft --cruft-expiration=<grace> - Temporary keep refs hold every
ref_logtip from the grace window. Cruft expiry goes by object mtime, so hydrated packs keep their creation mtime, the leaseholder re-pins mtimes before each repack, and.mtimesfiles travel through the bucket. - A full-repack publish increments
full_repack_gen, which fails any push that read the old generation. The publish aborts ifref_logadvanced during the repack. Geometric repacks drop nothing and need no fence. - The pack lifecycle is not prototyped yet. M2 prototypes it, with property-based and fault-injection tests, before production use (ADR 0004).
- A repack publish checks the lease epoch, so a stale leaseholder can't publish.
- Superseded, lost and expired pending packs go to GC (§5.6), with
their side files; superseded
multi-pack-indexgenerations go too.recoveredpacks never do. Commits and annotated tags that reindex could not place stay protected as keep roots, without blocking repacks, until an operator restores or discards them.
- gitoxide stays in-process off the wire path, for Hub API object reads, the reachability check and Hub commit writes. It only ever opens a request snapshot, never the node cache.
- git ≥ 2.37 is a runtime dependency (§9). Container images ship it from the distro (GPL-2.0, run as a separate program).
- Measured (
docs/research/S9-git-engine.md):- A full clone of a 10k-commit repo cost git 0.18 s CPU, against 2.86 s for an in-process gix server. gix 0.90 has no server side, no delta compression and no bitmaps.
- Concurrent pushes across two nodes produced exactly one winner in 60/60 rounds.
This is the hardest new component; budget for it accordingly.
10.10 Minimal self-service web UI (decided by the user: v1)
An optional web role. Small, fast and accessible, served by the same
binary.
What it does:
- Sign in through the operator's identity system (OIDC). No local accounts.
- Create, list and revoke access tokens; add and remove SSH keys.
- Browse repos, branches, commits and files; download files; copy clone,
hfandopenxetcommands. - See storage usage per repo and tenant: logical bytes, stored bytes, and the dedup savings ratio.
- Admin view (for operator-designated groups): health, GC status and dry-run reports, pools, mirror status.
Explicitly not included: model cards, README rendering beyond plain text, discussions, social features, previews of model or dataset contents.
Easy to restyle and disable; platforms like aqora will typically run their own UI and leave this role off.
11. Spikes (verify, don't assume)
All ten spikes are answered: S1–S9 in M0, S10 for M1. Each note records versions, method and what was observed versus inferred.
- S1 (answered: yes, on MinIO;
S1–S2, ADR 0002):
hf_xetdownloads correctly when v2 reconstruction returns native S3 presigned URLs with one range per fetch entry. - S2 (answered:
http://works for Hub, CAS and store; a private CA works viaSSL_CERT_FILEon Linux but not forhf_xeton macOS; S1–S2):hf_xetandhuggingface_hubaccepthttp://CAS and Hub endpoints for local dev, or what TLS setup the quickstart needs. - S3 (answered: 99 exchanges captured as fixtures in
compat/fixtures/hub/; corrections applied to §3 and §6:DELETEfor repo delete,validate-yamlon the upload path, tokens in body and headers; S3): the exactwhoami-v2,preupload,commitandtreeresponse shapeshuggingface_hubrequires, captured as fixtures. - S4 (answered: no for DuckDB and polars
hf://, yes for fsspecHfFileSystem; plain HTTPSresolveURLs work for all; S4): whether DuckDBhf://(and polars or fsspec) can target a custom endpoint; if not, document the plain-HTTPS URL route. - S5 (answered: yes; everything the server needs is public in
xet-core-structures1.7.0, at the cost of a heavy dependency graph, sync-only parsers and API churn within 1.x, contained by an exact pin behindopenxet-protocol; S5, ADR 0001): whetherxet-core-structuresis usable server-side, or which pieces to vendor or reimplement, and its versioning on crates.io. - S6 (answered: twelve rules, all checkable from per-xorb chunk hashes
and lengths stored at xorb ingest, with work caps per shard; reference
and real
hf_xetshards pass; what stock clients actually send is in §3; S6): the shard upload verification rules (spec "shard" page) implemented against the reference files. - S7 (answered:
git-xet0.2.1 has no download handshake, and downloads must answerbasic; withlfs.urlon another host, a signedhrefplusaccess=nonemakes the refresh work over HTTPS and SSH (§10.2);git-lfs-authenticatemust includehrefandexpires_in;git-lfs-transfercan't carryxet; S7, ADR 0003): thegit-xetdownload handshake, plusgit-lfsbehaviour with a separatelfs.urlhost (credentials,basicfallback), against a real Gitea in CI (the CI leg is specified in ADR 0003). Includes credential sourcing (§10.2) over both HTTPS and SSH, and whethergit-lfs-transfer(pure SSH) can negotiate thexetcustom transfer. - S8 (answered: the Hub read surface plus CAS reconstruction, with
every URL and token re-issued by openxet; HF reconstructions can be
fetched with per-repo 900 s read tokens, but their 3600 s,
range-contained,
xip-tagged URLs must not be cached or handed out, so the mirror ingests verified chunks into local xorbs; the legal texts best support self-hosted mirroring of public repos, while a hosted third-party mirror and shared gated access are high-risk and need a lawyer; policy decided in R19; S8, §10.6): HF pull-through mirroring. Which HF endpoints, and what Xet read-token behaviour, a mirror depends on, and the terms-of-use constraints for caching HF content. - S9 (answered: the
gitbinary serves the protocol on per-request snapshot repos, over openxet-owned database refs and bucket packs, with gix in-process off the wire path; gix 0.90 has no upload-pack or receive-pack server, no delta compression and no bitmaps, and cost 16x git's CPU to serve a 10k-commit clone; database CAS gave exactly one winner in 60/60 concurrent-push rounds across two nodes; JGit DFS, go-git, libgit2, Gitaly and Mononoke were assessed and rejected; S9, ADR 0004): Git hosting engine: gitoxide server-side capabilities (upload-pack/receive-pack, protocol v2) against thegitbinary on a cached bare repo. Measure throughput and latency, and check that bucket-backed packs keep nodes stateless. - S10 (answered: stock
hf_xet1.7.0 uses seven CAS routes in a storage-only upload and download; writes are retried whole,404on a/v2/route means fall back, tokens refresh only by clock, data URLs carry no credential, and client shard caches live 21 days; S10): the exact CAS wire contract that stock clients rely on, and thexet-dataAPI our own client calls (ADR 0006).
Client version floor (builder-check call): the oldest huggingface_hub,
hf_xet and huggingface.js versions that use the v2 reconstruction and
current token flow. They are stated in the compatibility matrix and tested
as the "oldest" leg of the suite. M0 did not pin them. The M1 plan sets
hf_xet 1.7.0 as the floor (S6, S10).
12. Milestones (v1 = all of them)
Release cut (decided by the user): build straight through M0–M7 to 1.0,
with no pause between milestones to wait for outside feedback. Real-world
feedback takes time to arrive, so waiting on it would only stall the work.
Until 1.0, the compatibility suite, benchmarks and the measured
time-to-first-upload stand in for users. The repo is public while it is
built, so anyone can try it early, but no milestone waits on that. The work
is built by the user with coding agents, so the plan sequences milestones
but sets no calendar dates.
-
M0:
- repo,
AGENTS.md, CI skeleton, ADR template - spikes S1–S9
- repo,
-
M1: CAS core on SQLite with local disk and static tokens.
- Storage-only mode passes
hf_xetupload and download, plus the golden vectors. - The stock-client paths of §3:
/v1/chunks/default/{hash},/v1/telemetrydropped,/v1/xorb-grants/*answering404. - Fuzz targets for the xorb and shard parsers.
- The M1 rows of the security tests (§5.9).
- Storage-only mode passes
-
M2: Hub-compat layer and Git hosting.
hf auth login,hf upload,hf downloadand listing work unmodified.git clone/git pushover HTTPS against openxet-hosted repos, sharing one history with Hub API commits (§10.9).- The Git LFS endpoint works:
git push/git clonewithgit-xetagainst a Gitea-hosted repo, andbasicdownload for clients withoutgit-xet. - The pack-lifecycle prototype and the ADR 0004 test checklist.
- The resolve-bytes subset of the HTTP read gateway (§5.5): the LFS
basicdownloadhrefs point at it (ADR 0003). openxet purge <file-hash>(§5.4).- One router: the CAS and Hub routes composed at the builder level under a single fallback, since axum can't merge two routers that each carry one (issue #46).
-
M3: production backends.
- Postgres; S3, GCS and Azure; presigned downloads.
- The range-binding probe against real AWS S3, R2, Garage, GCS and Azure, and the presigned-URL signing benchmarks (ADR 0002).
- OIDC and webhook auth.
- A multi-node scale test.
- A migration compatibility policy, so nodes of two adjacent versions share one database through a rolling upgrade (issue #47).
- Index round trips outside the CPU permits, once the index is networked (issue #48).
-
M4: operational completeness.
- GC,
reindexfrom bucket,verify. - Readiness that checks the index and the bucket (issue #49).
- Metrics, tracing, and the rest of the HTTP read gateway.
- Metering (§5.7) and the admin API that exposes it.
- Server-side chunking for non-Xet clients.
- Git over SSH (
git-lfs-authenticate,git-lfs-transfer). - The
git-xetsetup of §10.2: the server-side capability route, plus the upstreamgit-xetcontributions listed there. - Upstream: Xet download support in
git-xet(§6.1). This is contributed alongside the othergit-xetcontributions and coordinated with Hugging Face before any PR is filed.
- GC,
-
M5: reach.
bucket,spaceandkernelrepo types.- Cache tier.
- HF pull-through mirror.
- Multiple storage pools, residency and KMS.
-
M6: our own client (§10.8).
GET /v2/file-chunk-hashes/{file_id}, whichsyncneeds (until then it answers404; no stock client calls it).sync,mount, storage-only mode,login,setup git(including the Git LFS download agent),doctor.- Python bindings.
- The minimal self-service web UI (§10.10).
- The two-way compatibility suite, including nightly runs against huggingface.co.
The client's core (upload/download on
xet-data) starts as early as M1, because it doubles as our own test driver. -
M7: distribution and docs.
- Release pipeline, images, Helm (operator only if needed), compose, one-click templates, Nix, Homebrew, winget, PyPI, marketplace listings.
- Docs site.
- Published benchmarks.
- Time-to-first-upload check.
13. Left to the builder
- Exact crate layout and names, HTTP framework, DB access library. Decided in M1: axum, sqlx (M1-R1); EdDSA JWT tokens (ADR 0007).
- Small-file threshold, cache design, partitioning scheme details.
- Global-dedup response shards are decided (M1-R4, M1-R30): synthesized
from the xorbs of the newest visible shard that references the xorb
holding the queried chunk, with a fresh random HMAC key and a
shard_key_expiryof now + 7 days. - Docs site tooling.
14. Name
The project starts as openxet (decided by the user). Because it uses the "Xet" name, there is some trademark risk. Keep brand-specific strings in a few places (binary name, image name, docs domain) so a rename stays cheap if it is ever needed.
Prior art found on 2026-10-23: another repository already uses the name
(ggoggam/openxet, Apache-2.0, 8 stars). The GitHub organization openxet
exists (created 2025-08-15, empty) and belongs to someone else, so our GitHub
organization needs a different handle; that is the user's choice. For now the
repository lives at github.com/stubbi/openxet (decided by the user). The user
owns the openxet.com domain.
15. Hosted openxet.com (decided by the user: we must have the perfect hosted experience)
openxet.com is a first-class product, not an afterthought. "Perfect" is measured by the same anti-goals as the open-source project: fast, effortless, and listening. Detailed design is its own brainstorming round (pricing, legal, operations). These requirements are fixed now because they shape the core:
Experience:
- From the landing page to a working
hf uploadin under 60 seconds: sign in with GitHub, Google or SSO; a token is shown with copy-ready commands; done. - A free tier with no time limit, and no card needed to start.
- Every page shows what the user gains: stored vs. uploaded bytes and the dedup savings, in money where possible.
- Bring your own bucket as a headline option: we operate openxet, and the data stays in the customer's S3/GCS/Azure account. An EU region is available from day one.
- Git hosting,
git-xet,hf, our client and the web UI all work on openxet.com exactly as they do self-hosted. - No dead ends: every error says what to do next. Support is reachable from every page, and replies come within one business day.
Dogfooding rule: openxet.com runs the released open-source build. There is no private fork and no hosted-only core feature. Hosted-only parts are operational: billing, sign-up, abuse handling, the status page. They live behind the same seams (authorizer webhook, metering, admin API). Anything openxet.com needs from the core is built into the core, which makes self-hosting better too.
What this adds to the core:
- Hard multi-tenant isolation (per-scope dedup visibility, pools per tenant, noisy-neighbour limits).
- Enforced quotas and per-tenant rate limits.
- Bounds on how much one writer's registrations shape the dedup answers
other tenants get under
globalvisibility (issue #50). - Metering accurate enough to bill.
- Self-service sign-up via the web UI.
- Admin tooling for abuse and takedowns.
- Zero-downtime rolling upgrades.
Operations bar:
- A published SLO with a public status page.
- Tested backups and restores.
- Cheap-egress storage and a CDN, so it can be profitable.
- Abuse reporting and DMCA/DSA takedown processes, plus terms of service and a DPA, before public launch.
Truly useful, not just available:
- Two or three design partners (aqora first) run on openxet.com before billing is switched on.
- We measure their time to first upload and their dedup savings, and act on what they report.
16. Best in class on every dimension (decided by the user)
The ambition is that no other project comes remotely close, on any
dimension. An ambition is only useful if it is measured. So each dimension
below has a bar we hold ourselves to and a way we prove it, publicly where
possible. Competitors are re-evaluated every minor release in
docs/competitive-matrix.md. The first entries come from the 2026-10-23
evaluation of Shardline, ggoggam/openxet, kebyn/xet-server and
guilt/xet-server.
| Dimension | Bar | Proof |
|---|---|---|
| Protocol correctness | Byte-exact with xet-core; every spec MUST implemented and tested | Reference-file golden tests; real-client suite (pinned, latest, oldest supported); nightly against huggingface.co; spec-section traceability (each MUST maps to a test) |
| Performance | Fastest Xet server on upload, download, dedup ratio and metadata latency | Public benchmark suite run in CI against competitors on identical hardware; regressions fail CI; results published per release |
| Code quality | Small, readable, reviewed code with no slop | Strict clippy and forbid(unsafe); mutation testing (cargo-mutants) on core crates; fuzzing continuously (OSS-Fuzz once eligible); lines of code tracked as a cost, not a goal |
| Architecture | Every decision recorded and reversible | ADRs for every significant choice; crate boundaries enforced (no cyclic or upward deps, checked in CI); bucket-as-truth and stateless invariants have dedicated tests |
| Security | Secure by default, with a verifiable supply chain | The §5.9 requirements, each with its test; SECURITY.md with a response SLA; OpenSSF Scorecard ≥ 9; signed releases with SLSA provenance; reproducible builds; cargo-deny + cargo-vet; a third-party audit before 1.0 |
| Documentation | Every user finds the answer in under a minute | Diátaxis structure (tutorials, how-tos, reference, explanation); every example runs in CI; reference generated from code (CLI, config, OpenAPI); llms.txt; a docs feedback link on every page |
| Governance | Trustworthy beyond one person | GOVERNANCE.md (roles, decision process, path to maintainer); every change merged via PR with at least one independent review (human, or an agent reviewer plus human sign-off), required by process; branch protection enforces the PR, the required checks and linear history, but not the review (0 required approvals, enforce_admins: false) until a second maintainer exists (GOVERNANCE.md); an RFC process for big changes; DCO; code of conduct; a public roadmap |
| Community | Contributors feel welcome and succeed | Labelled good-first issues; contributor guide tested by newcomers; first response to issues and PRs within two business days; credit in every release's notes |
| Release engineering | Boring, predictable releases | SemVer; changelog generated from Conventional Commits; release train cadence; upgrade tests between consecutive versions; a stated support window |
| UX (humans and agents) | Best on both (§7.1) | Time-to-first-upload measured per release; the agent leg of the suite; accessibility (WCAG 2.2 AA) checked for the web UI |
| Operations / hosted | openxet.com is the reference deployment (§15) | Public status page and SLO; chaos and failover drills in CI; disaster-recovery restore tested on a schedule |
Bus factor is a real risk while one person leads the project. Governance work starts at M0, not after 1.0.
17. Rulings register
Rulings are decisions made while building, numbered in the order they were made. Public code and docs cite them by number; this is what each one decides, with superseded parts marked.
| Ruling | Decision | Where |
|---|---|---|
| R19 | The HF pull-through mirror ingests verified chunks only, never fetches or serves gated or private content with another caller's credential, and is disabled on openxet.com. | §10.6, S8 |
| R21 | Security issues found in third-party software are reported privately to its maintainers. | AGENTS.md |
| R22 | Git reads and Hub reads by revision serve only objects reachable from a current ref, on every path; ref writers obey the same rule. | §5.9 req 8, ADR 0004 |
| R24 | Objects a reindex can't place are persisted and protected, never discarded automatically, and never block repacks. | ADR 0004, S9 |
| R25 | Xet reads by hash follow scope membership, not Git reachability; removing data takes GC or openxet purge. | §5.4 |
| R26 | Public text never describes or hints at an unfixed third-party security weakness. | AGENTS.md |
| R29 | Upload validation composes upstream primitives with openxet's own policy checks; format logic is never reimplemented. | ADR 0001, S5 |
| R30 | Shard verification takes a synchronous chunk-table lookup, caps distinct referenced xorbs, and ignores unreferenced CAS-info xorbs. | S6 |
| M1-R1 | The CAS is axum on tokio, the index is sqlx (SQLite now, Postgres in M3), and the local-disk store is openxet's own fsynced module; object_store serves S3, GCS and Azure from M3. | M1 plan |
| M1-R2 | CAS tokens are EdDSA JWTs verified against a public key set; static tokens are compared by SHA-256 in constant time. | §5.4, ADR 0007 |
| M1-R3 | Signed transfer URLs bind xorb, range and expiry in an HMAC; a missing Range is 400, any mismatch 403. | §5.5, ADR 0002 |
| M1-R4 | Dedup responses are synthesized from the newest visible xorb containing the chunk, keyed with a fresh random HMAC key and a 7-day shard_key_expiry. The single-xorb part is superseded by M1-R30. | §13, §5.6 |
| M1-R5 | The GC grace defaults to 30 days and is never below 28 (21-day client shard cache + 7-day dedup key). | §5.6, S10 |
| M1-R6 | GET /v2/file-chunk-hashes is built in M6 and answers 404 until then. | §12, ADR 0006 |
| M1-R7 | Metering ships in M4 with the admin API. | §5.7, §12 |
| M1-R8 | Xorb validation runs on blocking threads bounded by one permit per CPU; double decompression on accept is kept and benchmarked. | M1 plan |
| M1-R9 | The client version floor is hf_xet 1.7.0. | §11, S10 |
| M1-R10 | The default bind is 127.0.0.1:8080, which is also hf_xet's default CAS endpoint. | S10 |
| M1-R11 | CAS tokens carry a scope, a mode and an expiry, never a Git revision: Xet reads follow scope membership (R25), so a revision binding would enforce nothing. Hub routes still accept a revision in the URL for compatibility. | §5.4, ADR 0007 |
| M1-R12 | Stored shards and dedup shards are built by composing xet-core's serializers (truncating at the file lookup section, rewriting the header or footer through them), never by writing format bytes by hand. | ADR 0001 |
| M1-R13 | An upload shard listing one file hash twice with different SHA-256 values is rejected; for identical duplicates the first entry is kept, and the CAS info lists only kept references. | S6 |
| M1-R14 | Shard verification itself rejects duplicate file hashes with conflicting SHA-256 values, so registration never meets a content error after verification passed. | S6 |
| M1-R15 | New dependencies take their latest stable release unless a ruling pins one; the index uses sqlx 0.9 (amends M1-R1). | M1 plan |
| M1-R16 | A file's size and terms are stored per (file, scope), and a scope reads only its own record; another scope's file reads exactly as one never stored. | §5.4 |
| M1-R17 | Registration rejects terms naming a xorb that is missing or not visible to the caller, with one error that doesn't tell the two apart, so registration is no existence probe. | §5.4, §5.9 req 4 |
| M1-R18 | CAS tokens are checked by openxet's own EdDSA-only compact JWS verifier on ed25519-dalek's strict verification, with a strict header and a size cap, tested against RFC 8037 appendix A.4. The dependency tree carries no ignored security advisory. | ADR 0007 |
| M1-R19 | CAS-token claims are sub, scope, mode, exp, iat and an optional, enforced nbf; any other claim is rejected, so a restriction openxet can't honour never fails open. Token lifetime is capped (24 h by default), iat may lie at most 5 min ahead, and a sub or scope that is empty, only whitespace or holds control characters is rejected, as is a scope over 1024 bytes. Its 24 h signed-URL cap is superseded by M1-R42. | §5.4, ADR 0007 |
| M1-R20 | Logs and spans record the matched route template, never the raw path or query, so no URL segment can leak a credential; a request is traced by its request id. | §5.9 req 6 |
| M1-R21 | An object the index names but the bucket lacks answers 500 with an integrity log, not a retryable 503: object stores are read-after-write consistent, so it means data loss or a bug, and a retry would hide it. | §5.3 |
| M1-R22 | A refused request's body is drained up to the route's size limit before the answer, as a lingering close does, so a client sees the final 401 or 403 instead of a connection reset it would retry; the body read deadline bounds the cost. | §5.9 req 1, routes reference |
| M1-R23 | Verification errors name the rule, the file, the xorb and the chunk range the client sent, and never a value derived from stored data (expected hashes, lengths, chunk counts); those are logged only. | §5.9 req 5, S6 |
| M1-R24 | Shard commits (bucket write through index registration) are bounded per process by a commit semaphore of 4 permits per CPU, separate from the validation permits; ingest memory is at most validation permits × the verification limits plus commit permits × a stored shard and its files. | M1 plan |
| M1-R25 | Blocking work (validation, verification, reconstruction rendering, dedup builds) takes its permit inside the blocking closure, so a client disconnecting can't release a permit while its work still runs. Dedup's use of the validate pool is superseded by M1-R32's own pool. | M1 plan |
| M1-R26 | Reconstruction trims the first and last kept terms to the chunks covering the requested range, as xet-core's reference server does, so fewer bytes are transferred. | S10 |
| M1-R27 | Ranged bucket reads are validated before the 206 and then streamed in 1 MiB blocks, so a transfer holds one block of its range at a time. | §5.5 |
| M1-R28 | Generated secrets are oxt_ followed by 43 base64url characters encoding 32 bytes from the operating system's CSPRNG; the fixed prefix makes leaked secrets findable by secret scanners. | §5.4 |
| M1-R29 | In a configured server, a transfer key missing at its default path is generated with a warning, and one missing at an explicitly configured path is an error; remote buckets (M3) will require a configured key, since every node behind one endpoint needs the same one. | §5.5 |
| M1-R30 | A dedup answer offers every xorb of the newest shard that references the matched xorb and is visible to the caller, in the shard's term order from the matched xorb, wrapping round, up to the answer budget of M1-R32 and without lookup tables; with no such shard, the matched xorb alone. | §5.4, S11 |
| M1-R31 | Independent tasks run in parallel, each in its own worktree, with review gates unchanged; from M2 on, plans give a dependency graph with file ownership. | §8 |
| M1-R32 | Dedup answers are at most 16 MiB (about 350 typical xorbs, about 22 GB of file) and hold a dedup permit, a pool of a quarter of the CPUs separate from validate, from loading their chunk tables until their body has been sent or dropped; dedup memory per process is at most those permits × about 150 MB (measured: about 120 MB for one answer, about 300 MB with 2 permits busy). | §5.4, S11 |
| M1-R33 | A dedup query waits at most 1 s for a dedup permit, then answers 429 dedup_busy with Retry-After: 1, which clients read as "no match"; sending an answer has a deadline of 30 s plus its length at 1 MiB/s, after which the body fails and its permit and memory are released. | §5.4, S10, S11 |
| M1-R34 | A reconstruction renders with extra blocking threads up to half of the free validate permits, counting the request's own permit, so one large answer never starves uploads. | benchmarks reference |
| M1-R35 | A required CI gate must pass 10 of 10 runs before it merges; a flaky gate is fixed, never loosened. | §8, benchmarks reference |
| M1-R36 | The documentation site is rendered from docs/ with a pinned mdBook at deploy time, and nothing generated is committed; llms.txt and per-page Markdown are generated, and CI builds and link-checks the site and requires every reference page in its navigation. | §7.1 |
| M1-R41 | This register lists every ruling that tracked code or docs cite. | §17 |
| M1-R42 | Signed transfer URLs live 30 to 60 minutes (60 by default), so a leaked URL, or one issued before a purge, stops working within the hour; supersedes M1-R19's 24 h URL cap. | §5.4, §5.5, ADR 0002 |