TL;DR: the entities the code.storage API exposes and the invariants a compatible backend must uphold. An organization owns repositories with refs in three namespaces; ref updates use compare-and-swap; several response details are load-bearing for clients.
Organization (org) ← tenant; from subdomain + JWT `iss`
└── Repository
├── repo_id: string ← internal ID (e.g. "repo_7f2b3d9" / UUID)
├── url: string ← client-facing URL path; may contain slashes ("team/project"); the value used in the JWT `repo` claim
├── default_branch: string ← defaults to "main"
├── created_at: ISO-8601 string
├── upstream? / base_repo? ← optional upstream link {provider, owner, name, ...}
├── git_credentials? ← stored HTTPS credential for generic upstreams
├── Refs
│ ├── refs/heads/* ← persistent branches
│ ├── refs/tags/* ← lightweight tags (annotated tags readable, not creatable via API)
│ ├── refs/notes/* ← git notes (default refs/notes/commits)
│ └── ephemeral namespace ← isolated ref space, exposed as {repo_id}+ephemeral.git
├── Commits (sha, parent_shas[], author, committer, date, message, signature?, payload?)
└── Trees/Blobs (mode: 100644 | 100755 | 120000 | 160000 | 040000)
- Multi-tenancy is host-based:
api.{org}.code.storage(API) and{org}.code.storage(git storage). The JWTissclaim must match the host's org. - Repositories have two identities: an internal
repo_idand aurlpath. The JWTrepoclaim carries theurl.GET /api/repo-urls/{repo_id}resolves internal ID → url (org:read scope, no per-repo claim needed). - There is no user model. Authentication is entirely customer-signed JWTs (see the auth spec); authorization is claims-based. The backend never stores accounts.
| Namespace | Git remote | Notes |
|---|---|---|
| persistent | {repo_id}.git |
normal branches/tags/notes; triggers webhooks + upstream sync |
| ephemeral | {repo_id}+ephemeral.git |
disposable branches; never mirrored/synced upstream; default-branch protection does not apply; promotable to persistent |
| import | {repo_id}+import.git |
bulk ingestion; pushed objects go (per official docs) to immediate cold archival |
The API addresses ephemeral refs with boolean flags, not ref names: ephemeral,
ephemeral_base, base_is_ephemeral, target_is_ephemeral, source_is_ephemeral. To
promote an ephemeral branch, call POST /repos/branches/create with
base_is_ephemeral: true, target_is_ephemeral: false.
Derived from SDK behavior and the real consumer (just-code-storage):
- Optimistic concurrency (CAS) everywhere.
commit-pack,diff-commit,restore-commit:expected_head_shavs current branch tip.merge:expected_target_shavs target branch tip.- notes write/delete:
expected_ref_shavs notes ref tip. - On mismatch: fail with HTTP 409, code
ref_guard_failed,guardandcurrent_sha. The commit writes keep theirresultobject{target_branch, branch, old_sha, new_sha: "", success: false, status: "conflict"}withcommit: null, and the notes writes keep the note envelope withresult.statusconflict. Clients (e.g.just-code-storagepush) map this to git's "failed to push some refs" UX, so the shape is load-bearing.
- Root-commit signaling. For a first commit on an unborn branch,
result.old_shais the all-zeros SHA (0{40}); the consumer detects root commits via/^0+$/. - Unborn branches. A new repo has an unborn default branch.
GET /repos/files/metadata(and file reads) on an unborn branch MUST return 404; the consumer falls back to an empty baseline forgit status. - Default branch protection.
DELETE /repos/branchesMUST refuse to delete the repository's default branch (persistent namespace only). - Ref policy enforcement. The JWT
refsclaim is an ordered list of[pattern, ops]rules; first match wins. Ops:no-push(reject all writes),no-force-push(reject non-fast-forward),verify-sig(reject unsigned/unverified commits). Enforced on both the git receive-pack path and the HTTP write endpoints. Short names normalize torefs/heads/<name>. - Read-only preview.
GET /repos/merge/previewmust never mutate refs or persist objects (conflict rendering may use request-local scratch storage). A conflicted preview returns HTTP 200 withstatus: "conflicted"; a conflicted actual merge returns HTTP 409. - Ephemeral isolation. Ephemeral refs never trigger upstream sync/mirroring or webhooks intended for persistent refs. Deleting ephemeral branches skips default-branch protection.
- Deletion is soft-first.
DELETE /repos/deletesoft-deletes, then cleans up hot/cold storage asynchronously; repeat deletion returns 409 ("already deleted").- Here the async cleanup is the retention purge
(
OPENSTORAGE_REPO_RETENTION/openstorage purge-repo). It removes the repository's storage permanently, with no restore. - The name burn persists: URL-scoped tokens may outlive any retention, so the
(org, url)pair is never released.
- Here the async cleanup is the retention purge
(
- Commit ordering.
GET /repos/commitsreturns history newest-first by committer date, cursor-paginated. - Tag semantics.
POST /repos/tagscreates lightweight tags only;targetmust resolve to a commit. List/delete must dereference annotated tags correctly (the listedshais the dereferenced commit SHA). - Upstream sync. Repos created with
upstream(or the deprecatedbase_reposync form) mirror an external provider (github,gitlab,bitbucket,gitea,forgejo,codeberg,sr.ht; self-hosted instances viaupstream.hostorupstream_host, credentials via git-credentials).- Non-ephemeral ref mutations on synced repos schedule background sync, unless the
writing token carries a ref-policy op (see
Ref policies fail closed).
pull-upstreamreturns 202 (async);DELETE /repos/basedetaches idempotently. - The sync lifecycle emits
repo.sync.started/succeeded/failedwebhooks. - At most one sync run per repo is in flight. A
pull-upstreamaccepted during a run is never dropped: it coalesces into one follow-up run scheduled when the in-flight run finishes. So a 202 always means a sync run will observe upstream as of the request or later. - The in-flight claim is leased (holder token + expiry derived from the run budget). If the holder dies mid-run, the claim expires and the next run steals it atomically, with no operator intervention. Holder-token guards refuse a stolen run's late writes. A crash can delay syncing but never permanently stop it.
- Non-ephemeral ref mutations on synced repos schedule background sync, unless the
writing token carries a ref-policy op (see
Ref policies fail closed).
- Forking.
base_repo: {provider: "code", operation: "fork", auth: {token: <jwt>}}forks an existing code.storage repo (optionally atref/sha) into an independent repo. The fork-source JWT carriesgit:readon the source repo. Upstream's copy form,base_repo: {name, ref?, auth: {token}}withoutprovider, runs the same fork;namemust name the repo the token grants.
- Diffs: endpoints return per-file pre-formatted unified diff text in
files[].raw; consumers concatenate these directly (git show,git diff).gitApplyCompatible=truemust producegit apply-able patches. - File reads:
GET /repos/fileresponses must includeX-Blob-ShaandX-Last-Commit-Shaheaders, a strongETag(the quoted blob SHA),Last-Modified,Accept-Ranges: bytes, and supportRange(206),If-None-Match(304),If-(Un)Modified-Since,If-Match(412),If-Range(416 on bad ranges). - Commit lookup:
GET /repos/commit?sha=resolves branch names, short SHAs, and full SHAs; the consumer'srev-parsebuilds on this. - Blame: line numbers are 1-indexed and map 1:1 to the file content at the blamed ref.
- Grep: results may be asynchronously indexed; the vendor's own integration test polls after a commit until matches appear. An open-source backend MAY grep synchronously: strictly fresher than the reference implementation, which is safe for clients.
- Signing-key registration for
verify-sig(no public endpoint; likely control-plane). - Exact cursor token encoding (opaque to clients; any stable encoding works).
- Grep/archive 503 "warming from cold storage" behavior implies hot/cold tiering; an open-source backend can serve everything hot and never emit these.