Developer accounts
A publish token is not a project credential. One npm token publishes every package you own; one Hugging Face write token reaches every model repo on the account. Copy that into each project's vault and you have not stored one credential — you have stored fifty copies of it, and rotating means finding all fifty.
So it lives on its own record, and projects point at it. Same reasoning as the Supabase account tokens and the AI provider accounts: when a credential authorises an account, it belongs to the account.
Available since backend 2026.09.01.1.
What it holds
Twenty-two providers ship declared. GET /developer-accounts/providers returns the live list — call that
rather than trusting this paragraph, because adding a provider is a config entry and does not need a release
note.
| Group | Providers |
|---|---|
| Package registries | npm, pypi, crates_io, rubygems, packagist, nuget, maven_central, homebrew_tap |
| Developer platforms | huggingface, github, docker_hub |
| Extension & plugin stores | chrome_web_store, vscode_marketplace, firefox_addons, edge_addons, jetbrains_marketplace |
| Hosting & infrastructure | cloudflare, vercel, netlify, expo, sentry |
| Own products | native_update |
Each provider declares its own fields, because providers disagree about what a credential is: npm takes a token, Docker Hub a username and a token, Maven Central a Sonatype token pair and a GPG passphrase.
A provider name may also be a project-vault service
Six of these names — github, cloudflare, sentry, native_update, and the extension stores — also exist
as services in a project's vault. They are not duplicates, and merging them loses the
distinction that matters:
- the project vault row is that project's configuration — a Sentry DSN, an extension id, a repo name, an OTA app id and its per-app device key. One row per project, and much of it is safe in a bundle.
- the developer account is the account-wide credential that can administer every one of them.
A DSN says where errors go; an auth token can delete the project. Storing them together is how the wrong one gets sent.
A field may declare a format, and both write planes enforce it
Since 2026.09.04.1, a provider can declare the shape its own backend requires, and a value that does not
match is refused on write with 422 INVALID_DEVELOPER_ACCOUNT_CREDENTIAL:
{
"error": {
"code": "INVALID_DEVELOPER_ACCOUNT_CREDENTIAL",
"message": "A native-update access token looks like nu_pat_ followed by 64 hex characters and a 4-character checksum. An app API key (nu_app_…) authenticates the device update plane and is rejected here on format.",
"details": { "provider": "native_update", "field": "token" }
}
}
details names the provider and the field and never the value — a validation error that echoes what was
sent puts a credential into the response body, your log and your CI transcript.
Only native_update.token declares one today. It exists because that platform mints two token families whose
names differ by four characters, one of which is meant to ship inside an app; sending the wrong one fails on
format at your first publish, months later, with an error that reads like a revoked token.
The scope
can_read_developer_accounts list · show · reveal
can_write_vault create · edit · assign to projects
🔴 Nothing else implies can_read_developer_accounts. Not can_reveal_vault, which is granted so a
caller can read one project's configuration. Not can_read_ai_accounts, which is granted so a caller can
spend an AI balance. Neither is a reason to publish a package under your name — a publish is public,
permanent and irreversible, and no registry offers an undo.
It defaults to off, so a token that has never been edited gets:
{ "error": { "code": "TOKEN_PERMISSION_DENIED",
"message": "This access token cannot read developer accounts…",
"details": { "required_scope": "can_read_developer_accounts" } } }
Turn it on in the admin under Access Tokens, then confirm with GET /token.
Assigning an account to a project needs only can_write_vault, because an assignment is a pointer — those
responses never contain a value.
Endpoints
| Call | Scope | Notes |
|---|---|---|
GET /developer-accounts/providers | any valid token | The registry: every provider, its fields, which are secret, the env var each maps to. Describes the schema, never a record |
GET /developer-accounts | read | ?q= · ?provider= · ?active=. Paginated, default 20, max 50 |
POST /developer-accounts | write | provider is required and must be one the registry declares |
GET /developer-accounts/{account} | read | {account} = numeric id, the identifier, or provider:identifier |
PATCH /developer-accounts/{account} | write | Answers with the whole record plus changes (field names only) |
POST /developer-accounts/{account}/reveal | read | The values, the env block, and the config files |
GET|PUT /developer-accounts/{account}/projects | read / write | One account → many projects |
GET|PUT /projects/{project}/developer-accounts | vault read / write | One project → many accounts |
The reveal is the point
A reveal does not just hand back a string. It hands back the thing you were going to build from the string:
curl -X POST https://fileshub.zaions.com/api/public/v1/developer-accounts/npm:aoneahsan/reveal \
-H "Authorization: Bearer fh_pat_..."
{
"data": {
"provider": "npm",
"has": { "token": true, "username": true, "registry": false },
"complete": true,
"credentials": { "token": "npm_…", "username": "aoneahsan" },
"undecryptable": [],
"env": { "node": "NPM_TOKEN=npm_…" },
"config_files": { ".npmrc": "//registry.npmjs.org/:_authToken=npm_…" }
}
}
Write what you are given. A job that re-derives //registry.npmjs.org/:_authToken= gets the prefix subtly
wrong once — a missing leading //, the wrong registry host — and then cannot publish, while the error
blames authentication. One line, in CI:
npm config set //registry.npmjs.org/:_authToken \
"$(curl -s -X POST "$FH/developer-accounts/npm:aoneahsan/reveal" -H "Authorization: Bearer $FH_PAT" \
| jq -r '.data.credentials.token')"
npm whoami # the whole test: prints your username, or 401
🔴 env has a node key and never a vite or next one. Every field on a developer account
authenticates a publish, so there is no browser-safe value here for such a block to contain — unlike the
project vault, which legitimately holds both kinds and has to filter. A
VITE_/NEXT_PUBLIC_ variable is inlined into a bundle, and an npm publish token in a bundle is a public
npm publish token.
Reading the response
has— which credentials exist, without decrypting any of them. Use it to decide whether a reveal is worth making.complete— whether every field the provider marks required has a value. An incomplete account is storable on purpose: you should be able to save half a credential and come back to it.undecryptable— field names that hold a value which could not be decrypted (an app key was rotated).hasstaystruewhile the value isnull, because set but unreadable and not set need different fixes. This is a list, so it is[]when empty.envandconfig_filesare maps, so they are{}when empty — which is the normal state of a brand new account. (They were[]in2026.09.01.1; fixed in2026.09.01.2.)
Serving every project
An account reaches a project two ways:
| Mechanism | Meaning |
|---|---|
PUT /developer-accounts/{account}/projects with {"project_ids": [...]} | An explicit set. [] detaches everything |
PUT …/projects with {"all_existing": true} | A snapshot of every project that exists right now. A project created tomorrow is not covered |
PATCH /developer-accounts/{account} with {"all_projects": true} | A standing rule — every project, including future ones |
Sending all_existing and project_ids in one body is a 422, not a resolved precedence: "the snapshot
wins" and "the explicit list wins" are equally defensible, so you should never have to guess which was built.
For one npm token that publishes the whole fleet, all_projects is almost always what you want. Each
resolved entry reports via: explicit | all_projects, which tells you where to undo it.
🔴 A name collision worth knowing: an access token also has an all_projects field, meaning which
projects you may reach. They are unrelated, and an account flagged for every project never widens a
token's scope — a project-scoped token still sees only its own projects listed under that account.
Where this is not the answer
- Play Console and App Store Connect are deliberately absent. Their signing material is genuinely per-app and lives in the project vault; an upload needs an artefact you sign yourself, so there is no unattended publish for a stored credential to serve.
- Six provider names also exist as project-vault services —
github,cloudflare,sentry,chrome_web_store,firefox_addons,edge_addons. They are not duplicates. The vault row is that project's configuration: a Sentry DSN, an extension id, a repo name. The developer account is the account-wide credential that can administer every one of them. A DSN is safe in a bundle; an auth token can delete a project.