How it works
The shape of the system
Section titled “The shape of the system” ┌──────────────────────────────────────────┐ Browser ────────▶│ cavs-web React dashboard │ └────────────────┬─────────────────────────┘ │ /api/v1 cav CLI ─────────┐ ▼ Python SDK ──────┼───────▶┌──────────────────────────────────┐ TypeScript SDK ──┤ │ cavs-api │ git-lfs ─────────┤ │ control plane + data plane │ CAVS LFS agent ──┘ └───┬─────────────┬────────────┬───┘ │ │ │ MongoDB ─┘ │ └─ Pub/Sub (metadata) │ │ ▼ ▼ Cloudflare R2 / your S3 ┌──────────────┐ (object bytes, presigned) │ cavs-worker │ ▲ └──────┬───────┘ │ │ └── direct client transfer ──┘ (bytes never pass through the API)Three deployables:
| Service | Role |
|---|---|
cavs-api |
HTTP API under /api/v1. Control plane (identity, orgs, repos, RBAC, tokens, billing, usage, audit) and data plane (upload sessions, download authorization, Git LFS endpoints). |
cavs-worker |
Pub/Sub consumers and scheduled maintenance: usage reconciliation, notifications and email, outbound webhooks, snapshots, storage health probes, recommendations, GC/repack coordination. |
cavs-web |
The React dashboard. In production it also reverse-proxies /api to cavs-api, which is why the API and the app share the cavsnode.com origin. |
State lives in MongoDB (all metadata) and an S3-compatible object store (all bytes — Cloudflare R2 for managed storage, or your own bucket under BYOS). Events flow over Google Cloud Pub/Sub on a single topic, consumed idempotently.
Control plane vs data plane
Section titled “Control plane vs data plane”This split is the single most important thing to understand about the API.
- The control plane answers questions about your data: who you are, which repositories exist, what’s in them, how much they cost. Every call is a MongoDB read or write. Bodies are capped at 1 MiB.
- The data plane moves bytes. It never runs control-plane queries per byte. A request resolves the repository and your permissions once, then hands you presigned object-storage URLs and steps aside. Your client uploads or downloads straight to R2/S3.
Objects are content-addressed
Section titled “Objects are content-addressed”Every stored object is identified by its oid: the lowercase hex SHA-256
of its raw bytes (64 characters). There are no file paths in the data plane —
the storage key is derived from the oid:
<repository namespace>/objects/<oid[0:2]>/<oid[2:4]>/<oid>Consequences worth internalising:
- Uploading the same bytes twice is free. The second upload resolves to the same key.
- Renaming a file changes nothing in storage. Paths live in the Git index layer on top (see below).
- Verification is inherent. A download that doesn’t hash back to its oid is a corrupt download, and every client rejects it rather than writing it.
The two data paths
Section titled “The two data paths”Your Git host keeps the source and history. CAVS becomes the LFS backend by
setting lfs.url to the repository’s /lfs endpoint. Two flavours:
Standard git-lfs — the basic transfer adapter. POST /lfs/objects/batch
returns presigned upload or download URLs per object. Zero extra software, full
whole-file semantics, dedup only at the object level (identical files stored
once). See Git LFS.
CAVS transfer agent — cavs-lfs-agent registered as a standalone custom
transfer agent. It replaces LFS’s transfer and storage entirely: on push it
chunks each object, dedups against a shared store and exports a
content-addressed static tree; on pull it fetches only the chunks the local
cache is missing, using HTTP range requests. This is where the chunk-level
savings come from. See CAVS LFS agent.
No Git involved. An upload session brackets a batch of objects:
POST …/uploadsopens a session (send anIdempotency-Key).POST …/uploads/{session}/objectswith{oid, size}per object returns a presignedupload_urlfor each.PUT <upload_url>streams the raw bytes to object storage.POST …/uploads/{session}/objects/{oid}/completerecords the object and meters usage.POST …/uploads/{session}/finalizepromotes the objects, bumps the repository generation and emits events.
Downloads are one call: POST …/downloads/authorize with a list of oids
returns a presigned download_url for each. See
Upload sessions and Downloads.
What a Git push actually does
Section titled “What a Git push actually does”With the CLI connected (cav repo connect), a single git push triggers three
independent things:
-
Git pushes commits and trees to your Git host, as always. CAVS is not involved.
-
Git LFS hands each large object to the CAVS agent, which chunks it, dedups it against the repository’s shared store, and
PUTs the resulting content-addressed tree files to…/lfs/*. When the batch is done it callsPOST …/lfs/finalize, which registers the objects with their real post-dedup footprint, bumps the generation, and publishesrepository.usage.changed. -
The pre-push hook uploads the Git index.
cav hook pre-pushwalks the refs being pushed and streams commits, tree entries, branches and tags to…/index/sessions/…. This is what makes the dashboard able to show file paths, commit history, branches and releases on top of an oid-only data plane. It is best-effort and never blocks the push.
Then, asynchronously, cavs-worker reconciles the repository’s real physical
footprint from the object store, updates the usage snapshot the dashboard
reads, evaluates quotas, refreshes health checks and recommendations, and fans
matching events out to your webhooks.
The Git index layer
Section titled “The Git index layer”The data plane knows only oids. The index is a read-only, Git-semantic
layer over it, populated by cav repo index and the pre-push hook:
| Collection | Holds |
|---|---|
repo_commits |
Commit metadata + which LFS artifacts each commit touched |
repo_refs |
Branches and tags, with a pointer to the current tree generation |
repo_tree_entries |
A snapshot of the file tree per generation (paths → oids) |
repo_releases |
Releases derived from tags, with editable name and notes |
repo_events |
The activity feed |
Every read endpoint that depends on it degrades gracefully: if a repository has
never been indexed, responses carry "indexed": false and the UI prompts you
to run cav repo index --full.
Events and workers
Section titled “Events and workers”One Pub/Sub topic carries every internal event, keyed by repository id so
per-repository events stay ordered. Delivery is at-least-once and consumers are
idempotent (a processed_events collection dedups by event id).
A subset of internal events is mapped to public webhook events — only those ever leave the platform. The full list is in the Event catalog.
- Core concepts — the vocabulary, precisely defined.
- Architecture — deeper detail, for operators.
- Deduplication explained — why the numbers look the way they do.

