Skip to content

How it works

┌──────────────────────────────────────────┐
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.

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.

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.

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 agentcavs-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.

With the CLI connected (cav repo connect), a single git push triggers three independent things:

  1. Git pushes commits and trees to your Git host, as always. CAVS is not involved.

  2. 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 calls POST …/lfs/finalize, which registers the objects with their real post-dedup footprint, bumps the generation, and publishes repository.usage.changed.

  3. The pre-push hook uploads the Git index. cav hook pre-push walks 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 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.

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.