user_id — the account that authored it. Look at any Blob, any
Revision, or any message posted into a workflow thread, and that field is what tells you who is responsible.
Today, that user is always a person, because the only way to authenticate programmatically is an API key, and
every API key belongs to whoever minted it. A service account is what changes that.
What a Service Account Is
A service account is a machine identity that is a user, not a special kind of API key. It gets its ownid, from the same id space every human user gets. It shows up wherever a user_id shows up — member lists,
author fields, thread attribution — because as far as the rest of the platform is concerned, it is one.
Contrast that with what happens today. When you mint an API key, you mint it as yourself. Every Blob a CI
job creates with that key, every Revision a deploy script commits, every message a bot posts into a thread —
all of it records your user_id, because the key never had an identity of its own to give. It carries
authority (what the holder is allowed to do) but not identity (who is actually doing it). Three different
scripts sharing one key all look, in the history, like one person.
Give the work its own service account instead, and a Revision it commits is authored by ci-deploy, not by
the engineer who set ci-deploy up. That’s the entire idea. Everything else on this page is either a
consequence of it or a rule for keeping it safe.
Ownership vs. Membership
Two different questions govern a service account, and BlobHub keeps them completely separate:- Who may administer it — rename it, mint or revoke its keys, retire it — is decided by ownership. Every service account is owned by exactly one user or one organization, decided at creation and fixed from then on.
- What it can reach — which organizations and Blobs it can read, write, or administer — is decided by membership, exactly the way it’s decided for a human. You add a service account as a member of an organization or a Blob, with a role, the same way you’d add a colleague.
ci-deploy doesn’t hand you
its memberships. And being a member of an organization grants a service account no say over its own
administration. Granting a service account access to something is the ordinary “add a member” flow. There
is no separate mechanism for machine identities — you pick the account the same way you’d pick a person.
The dashed line is administration; the solid chain is reach. Both start at the account, and they never touch
each other again.
Identity, Not Alias
A service account has:- An
id— a UUID, assigned the moment it’s created, from the same id space every user gets. - A
name— free-form text you choose, one to a few words. It does not have to be unique:ci-deploycan exist in ten different organizations at once.
id or its name; there is no alias to find.
The Recipe
Here’s the whole lifecycle end to end, using an organization’s own admin-scoped API key throughout, via theX-API-Key header — the same credential your CI pipeline would use. $ORG_API_KEY needs the admin role on
org_456.
1
Create the account
owner_target_target_id is an internal index key the API happens to serialize — safe to ignore, along
with target_target_id further down. type is the extension point: "service" is the only value today,
and a future kind of account will be a new value here, not a new endpoint.2
Add it as a member
Owning
org_456 doesn’t make ci-deploy a member of it — that’s the invariant above, in action. It has
to be added like anyone else:3
Mint its key
Same endpoint you’d use for an Organization or a Blob, targeted at the account instead:Two fields carry the whole feature.
user_id is the account — not whoever’s $ORG_API_KEY called this
endpoint — because this key authenticates as that principal. created_by_user_id is provenance: the
human on whose authority the key exists. They are never the same field; conflating them is exactly the
bug service accounts close, where a key’s user_id was always its creator, so a worker key always looked
like whoever set it up. key is returned once, in this response, and never again — store it now.4
See it write as the account
Anything from here on uses That’s
ci-deploy’s own key, not the org key:ci-deploy looking itself up. Notice there’s no alias key at all — not null, simply never
written. (role here is the caller’s own standing on whatever it just read, owner because it’s reading
itself — not a stored attribute of the account.) The same resolution happens on every write: a Blob this
key creates, a Revision it commits, a message it posts into a thread all carry author_id or user_id
equal to user_901 — ci-deploy, in the product’s own records, not whoever was behind $ORG_API_KEY.Key Reach
A key minted at the account is the account — there’s no extra scoping step, and nothing to configure beyond the account’s own memberships:- Add
ci-deployas a member of three organizations, and the one key from the recipe reaches all three. You don’t mint a separate key per organization it needs to touch. - Remove
ci-deployfrom one of those organizations, and that access is gone immediately, without touching the key at all. The key is still valid; it simply has one less place left to use it.
The Access Perimeter
A service account authenticated by its own key may do product work. It may not change who can access what. That one sentence is the whole capability model, and it is worth holding as a boundary rather than as a list of refusals, because the list will grow over time and the boundary will not. Product work is everything the platform is for: reading and writing Blobs, committing Revisions, running workflows, posting into threads, uploading session objects, listing anything it can reach. The account does all of it under its ownuser_id, at whatever role its memberships give it, with no special cases anywhere.
The access perimeter is the much smaller set of operations that decide who else can get in. Every one
of them refuses a service account’s own key with 403:
† The only conditional row:
PATCH is refused solely when visibility actually changes, and every other
field goes through untouched — see Publishing Is the Subtle One below.
The refusal never depends on role. An account holding an admin membership passes every access check that
membership implies and is still refused here, because admin is standing on a target and this rule asks a
different question — what is behind the credential — that no role can answer. Every refusal is the same
response:
Publishing Is the Subtle One
PATCH on an Organization or a Blob is not a perimeter operation. It is refused only when it actually
changes visibility, in either direction:
- Renaming, re-describing, re-tagging, setting a
url— always allowed, including in the same request as anything else. These are ordinary product work and stay that way. - Sending
visibilitywith the value already stored — allowed. It moves nothing, so there is nothing to refuse. This matters more than it looks: an idempotent client that PATCHes its whole desired state on every run would otherwise start failing forever the moment that state includedvisibility. - Flipping
private→public, orpublic→private— refused.
Impersonation Is the Escape Hatch
Nothing on the perimeter is permanently out of reach. It is out of reach of the key. Behind every operation in that table the platform asks one question — is there a human behind this request? — and the account’s own key answers no, always, whoever holds it and whatever role it carries. An acting token answers yes, and names them. Mint one as yourself and, for the next 60 minutes, you hold a token that authenticates as the account and carries you as the actor. It does everything the account’s key does, plus the perimeter operations its access reaches, because the request now has a human behind it — you, on the record, for whatever it does. That is the trade the whole design turns on: the perimeter is not sealed shut, it is made attributable. One operation the token does not carry all the way across is minting an API key at an org or blob target, which is refused while acting with400 and error: cannot_mint_while_acting. Such a key authenticates as
whoever mints it, so one minted here would record the account rather than you, and would then be refused by
every operation in the table above for the rest of its life. Mint it as yourself — an account is given reach
into an organization or a Blob by membership, never by that target’s own key. Minting the account its own
key, at target=user, is untouched — that is the operation acting mode exists for.
The hatch is deliberately unreachable from inside. A service account’s key cannot mint the first acting
token, and an acting token cannot mint another, so no automation can walk itself across the perimeter — a
human starts every chain, every time.
Why the Line Is Drawn Here
This is what makes it safe to hand a key to something you don’t fully control: a worker on a machine you don’t own, a third-party CI runner, a script pulled from somewhere else. If that key leaks, whoever holds it can act as the account within the memberships it already has — and nothing more. They cannot mint a longer-lived replacement, add the account to another organization, stand up a second account to survive your rotating the leaked one, or flip a private Blob public and walk away with a URL that outlives the key. Revoke the key and the access ends completely, in one step, because the leak never had a way to spread past it. (All of this is about the account’s own key. Administering the account — minting it a fresh key, for instance — is still perfectly possible, but only through the paths in the next section, none of which run through the key the account already holds.)Who May Administer One
Administering a service account — renaming it, minting or revoking its keys, retiring it — always traces back to a human, or to a credential a human has already trusted with broad authority over the account’s owner:- The owning user, signed in as themselves.
- An admin of the owning organization, when the account is owned by an organization rather than a person.
- An org-scoped API key minted with the
adminrole, for the owning organization. Every administrative operation on a service account requiresadminspecifically — awrite-scoped key won’t reach any of them. - An acting token, for the one account it acts as — and only that one. It carries the human who minted it, so it satisfies the perimeter rule, but it is scoped to the single account it names: it cannot administer a sibling account the same human happens to own.
Lifecycle
A service account startsactive the moment it’s created and stays that way until someone retires it.
Retiring one:
- Deletes every API key it holds.
- Deletes every credential stored on the account itself — third-party provider secrets at
target=user/{account}. Credentials stored on an Organization or a Blob are untouched: those belong to the Organization or the Blob and outlive every account that ever read them. - Removes every membership it holds.
- Sets its
statustoretired.
id stays
resolvable, so a Revision ci-deploy authored a year ago still shows ci-deploy, not a dangling reference.
Retiring an account ends its future; it cannot rewrite its past.
Restrictions
Three things a service account can never do, by design:- Own an organization, or another account. Creating either is rejected outright — with
service_account_cannot_own_orgorservice_account_cannot_own_accountwhere the request gets far enough to be told why, and a plain403where it does not. Anything owned by a service account would become unreachable the moment whoever administers that account lost access to it, with no way back in. - Change the access perimeter with its own key. Covered above, under The Access Perimeter — no exception for role, no exception for target, and the same answer whether it is granting, revoking, or publishing. Impersonation is the only way across.
- Sign in. There’s no password and no OAuth identity behind a service account — the only way to act as one is a key minted for it, or an acting token minted by a human.

