Artifacts reference

Config options

OptionDefaultMeaning
basePath/artifactsWhere the repository routes mount. Changing it changes every address this capability serves
tokenTtlSeconds900How long a minted git token lives, 60 to 3600. This is the revocation window. Above 3600 is refused at boot, naming the ceiling
managePowernoneThe organization power that lets a member manage the organization’s repositories, e.g. repos:manage. Required when organization is composed, and refused at boot when your role catalog does not declare it. Ignored without organization
acceptRepoNamefalseLet a create request supply the physical name. Off, every name is a random UUID. On, uniqueness is yours
bindingARTIFACTSThe Worker env binding the namespace is bound as

One seam rides beside them: repoName, a function of the owner and the display name that returns the physical name. With it, uniqueness is yours. A collision is artifacts/name_taken, and a name outside the rule is artifacts/invalid_name.

The floor is Cloudflare’s, the ceiling is the kit’s. Cloudflare refuses a token that lives under a minute. The hour is the kit’s judgment: removing a grant does not revoke a token already minted, so a long life is not a tuned timeout, it is revocation turned off.

Routes

Using Artifacts has the calls. Every route but the five under /admin/ is bearer · session, and denies without auth. The five are control-plane, and each takes the scope named in its row.

RouteWhoPurpose
POST /artifacts/reposany signed-in personCreate a repository they own
GET /artifacts/reposany signed-in personWhat they own and what they hold a grant on. Never a listing of the namespace
GET /artifacts/repos/:nameowner or grantThe row, and what Cloudflare holds. No remote with remote access off
DELETE /artifacts/repos/:nameownerDelete the repository, every token on it and every grant
POST /artifacts/repos/:name/grantsownerGive a person read or write, or change what they hold
DELETE /artifacts/repos/:name/grants/:userIdownerTake a grant away
POST /artifacts/repos/:name/credentialsowner or grantMint a token at the caller’s scope. 404 with remote access off
DELETE /artifacts/repos/:name/credentialsownerThe break-glass. Revoke every live token, every recorded one by its id and then from the listing
GET /artifacts/repos/:name/logowner or grantHistory, newest first. ref, limit (at most 1000, default 50), offset
GET /artifacts/repos/:name/commits/:hashowner or grantOne commit
GET /artifacts/repos/:name/tree/:hashowner or grantOne tree’s immediate children
GET /artifacts/repos/:name/blob/:hashowner or grantOne blob’s bytes
GET /artifacts/repos/:name/fileowner or grantOne file at a ref, by path
POST /artifacts/organization/reposmanagePower in the acting organizationCreate a repository the organization owns
GET /artifacts/organization/reposmanagePower in the acting organizationThe organization’s repositories
GET /artifacts/admin/reposcontrol-plane · artifacts:repos:readEvery repository, newest first, with its owner, remote access and last push. remoteAccess (true or false), ownerKind (org or user), ownerId, which needs ownerKind, cursor, limit
GET /artifacts/admin/repos/:namecontrol-plane · artifacts:repos:readOne repository, its grants, and its commit log. limit, offset
GET /artifacts/admin/repos/:name/tokenscontrol-plane · artifacts:repos:readCloudflare’s one page of live tokens — 30, newest first, with the total beside it — with scope, created, expiry, minted and the holder where the kit recorded one
POST /artifacts/admin/repos/:name/tokens/:tokenId/revokecontrol-plane · artifacts:tokens:revokeRevoke one token. idempotencyKey and reason required
POST /artifacts/admin/repos/:name/tokens/revokecontrol-plane · artifacts:tokens:revokeRevoke every live token, past the one page. idempotencyKey and reason required

The organization routes mount only where organization is composed. Without it, every repository is a person’s.

The /admin/ routes are a management client’s, reached only through the control plane. minted is pithy for a token the kit recorded, with its holderId; outside for one it has no record of, never given a holder; and unknown for one created before recording began, which the kit may have minted. The listing holds live tokens only, so every token on it is active. A revoke replayed under a spent key answers what the first did, replayed: true, and revokes nothing. The same key for a different revocation is core/conflict, 409. A token the kit minted that is already revoked or expired answers revoked: 0; any other token that is not live is core/not_found, like one the repository does not hold. No scope creates, forks or deletes a repository.

An owner is the person who owns the repository, or a member acting for the owning organization whose role holds managePower. A grant is a row: read or write. An owner implies write. No ownership and no grant is no access at all.

The token’s scope is the caller’s access. Owner and write mint write. read mints read. Nothing else decides it, so a read grant can never obtain a write token.

The credential response

{
  "token": "art_v1_…?expires=1792425600",
  "scope": "write",
  "expiresAt": "2026-10-19T17:00:00.000Z",
  "remote": "https://<ACCOUNT_ID>.artifacts.cloudflare.net/git/acme-prod/<name>.git"
}

It answers 201 with Cache-Control: no-store. The token is sent as Authorization: Bearer, or as the HTTP Basic password with any username. The kit never stores it, and no log line, error body or audit row carries it.

Error codes

CodeStatusMeaning
artifacts/not_found404No such repository, or one you neither own nor hold a grant on. Also the mint route for a repository with remote access off
artifacts/forbidden403A repository you can see and may not do this to. Or no organization in force, or a role without managePower, on the organization routes
artifacts/invalid_name400A supplied physical name outside the charset or the cap. Or a name sent to a Worker without acceptRepoName
artifacts/name_taken409The owner already holds that display name, or a supplied physical name is already in the namespace
artifacts/invalid_config400Raised at boot: tokenTtlSeconds out of range, or organization composed with no managePower, or one the catalog does not declare
core/not_found404A commit, tree, blob or file that is not in a repository you can see

The first one is byte-identical, whichever it was. The physical name is in the clone URL, and a refusal that told the two apart would hand it over. Which it was survives in detail, which never crosses the wire.

Response hardening

A repository’s bytes are whatever somebody pushed, served from your own origin. So the blob and file routes answer with:

HeaderValue
X-Content-Type-Optionsnosniff
Content-Security-Policydefault-src 'none'; sandbox
Content-Dispositionattachment

A blob is application/octet-stream. A file carries the type Cloudflare resolves for it.

Bindings

BindingTypeWritten by
DBd1pithy add
ARTIFACTSartifactspithy add at the top level, pithy provision in every other stanza

The entry is { "binding": "ARTIFACTS", "namespace": "<project>-<env>" }. A feature’s namespace is <project>-f<issue>-<slug>--artifacts. Nothing creates it. Cloudflare makes a namespace on its first repository.

Tables

pithy_artifacts_repos · pithy_artifacts_grants · pithy_artifacts_tokens · pithy_artifacts_revocations

TableKeyHolds
pithy_artifacts_reposthe physical nameDisplay name, owner kind (org or user), owner id, remote access, timestamps. UNIQUE (owner_kind, owner_id, display_name)
pithy_artifacts_grants(repo, user_id)One grant per person per repository, read or write, timestamps
pithy_artifacts_tokensthe token id Cloudflare assignsOne record per token the kit minted: the repository, the user it was minted for, scope, whether it came from a mint or a create, created and expiry. No column for the token itself
pithy_artifacts_revocations(connection_id, idempotency_key)One spent revocation key per management connection: what it revoked, the reason, and how many

A token record names who a token was minted for, which Cloudflare’s own token metadata never does. It is the kit’s note, never the credential: there is nowhere to write the plaintext, which Cloudflare returns once. A record outlives its token by thirty days, then is pruned. The migration that creates the table also writes one marker row, the moment recording began, so a token older than that reads as unknown rather than as minted outside Pithy.

A purge deletes every repository the person owns, with its contents and every grant on it, and every grant they hold on somebody else’s. Their token records are anonymized instead: the user id becomes an erased- stand-in, one per record, so a token still listed reads as the kit’s. The export hands back their repositories, grants, and token records under mints. A repository an organization owns is the organization’s.

A grant is decoded on read. A value that is not read or write is refused as no access, never asserted.

Audit actions

Recorded through audit when it is composed, and nowhere when it is not.

ActionWhen
artifacts/repo_createdA repository was created
artifacts/repo_deletedA repository was deleted, with every token and grant
artifacts/grant_setA grant was made or changed
artifacts/grant_removedA grant was taken away
artifacts/token_mintedA token was minted. Its id, scope and expiry, never its value
artifacts/tokens_revokedEvery live token was revoked: by the owner, or by a management client with a reason. Severity warning
artifacts/token_revokedA management client revoked one token, with a reason. Severity warning

An organization’s repository records against that organization as the tenant.

Naming

A repository has two names. The physical name is in the namespace and the clone URL. It defaults to crypto.randomUUID(), which cannot collide and cannot be guessed. The display name is a column: free text, at most 100 characters, unique per owner and nowhere else.

A supplied physical name is a letter or digit, then letters, digits, ., _ and -, 2 to 63 characters. It is refused, never shortened. The naming rules have the namespace’s own row.

Peer capabilities

Required: nothing.

Optional: auth, effectively required, since every route denies without it. organization, so a repository may be an organization’s, and managePower becomes required. audit, which records the seven actions above.

What is not here

No fork() or import(). The binding offers both, and neither is needed to hand somebody a repository.

No mapping of your roles onto read and write. That is a grant row you write.

No storage metering. GET /artifacts/repos/:name reports what Cloudflare holds. The limits are 1 GB a repository and 1 TB an account.

ESC