Skip to content

Authentication

Hadron's /mcp and /graphql endpoints accept a single header for authentication:

Authorization: Bearer <token>

The token is one of two formats — hdr_app_<…> or hdr_user_<…> — and which one you use depends entirely on who's calling and on whose behalf they're acting. This page explains the split, why both exist, and how to pick.

The two paths

Format Scope Issued at Use for
hdr_app_<64 hex> One App (in one Org) Portal → App's settings page; programmatic regeneration App backends serving multiple users; headless / IoT integrations; anything where the App is the principal acting on Hadron
hdr_user_<64 hex> One User (the issuer) Portal mint OR OAuth /token redemption Interactive MCP clients (Claude Desktop, VS Code); CLI scripts; one-off curl commands; anything where the person is the principal acting on Hadron

Both formats are 256-bit random tokens stored as SHA-256 hashes; the server validates either through the same middleware path and resolves to the appropriate principal (App or User). The middleware does no work that depends on which format it received — that consistency is load-bearing for the cross-format parity contract.

Why two paths

The split exists because the principal is different.

An App backend is itself the entity making decisions on Hadron's behalf. It runs on a server you control, holds a long-lived secret in its environment, and operates across many of your end-users (who may never have a Hadron account at all — they're using your product, not Hadron directly). The right authentication primitive for that is a single long-lived server credential per App: an AppKey.

A person at an interactive MCP client (Claude Desktop, VS Code, a CLI script you wrote) is acting as themselves. The credential needs to authenticate the human, and that human needs to be able to see + revoke every credential issued on their account. The right primitive is a per-user credential tied to a User row: a UserApiKey.

These are operationally different — but at the transport layer they're the same (Authorization: Bearer …), at the validation layer they share the same hashing primitive, and at the audit layer each row records who/what minted it (the issuedVia field on UserApiKey records portal vs oauth:<client_id>; AppKey rows are implicitly App-issued).

How UserApiKeys get issued

UserApiKeys come from two places:

Portal-minted (you click "Mint")

You open your portal API-keys page, click Mint, and copy the raw key once. Used for scripts, CLI tools, ad-hoc curl, anywhere you need a bearer token to paste into something. The issuedVia field reads portal on these rows.

OAuth-issued (an MCP client walks the dance)

You add Hadron as a Custom Connector in Claude Desktop or VS Code. The client walks the MCP OAuth 2.1 dance: discovery → DCR → PKCE-protected /authorize/token. The dance ends with the same hdr_user_<…> shape — issued to the same User, indistinguishable at the transport layer from a portal-minted key — but the issuedVia field records oauth:<client_id> so you can tell in the portal which OAuth client minted which token.

The reason both paths produce the same shape is deliberate:

One credential format at the server. Two issuance paths to it.

This is the D-2026-05-17-001 decision: OAuth-on-top. The server speaks ONE credential format — not two — so that the validation path is the same regardless of how the credential came to exist.

Cross-format parity

Concretely, this means:

  • A portal-minted token and an OAuth-issued token to the same User resolve through the same middleware and produce the same authenticated context.
  • Revoking either kind through the portal makes any further request bearing that token fail with 401 Unauthorized immediately.
  • A revoked token is indistinguishable from a never-existed token at the /mcp 401 response — same status, same headers, same body. No way for an attacker to probe "is this key revoked or just unknown?"

The contract is pinned by an integration test (025-credential-parity.test.ts) that mints both kinds for the same User, walks every observable layer (raw format, persisted row shape, middleware context, 401 after revocation) and asserts the only differences are the two documented discriminator fields (issuedVia + label).

Picking the right path

The decision tree:

  1. Are you an App backend serving multiple end-users? Use an AppKey. Live in the App's settings page; rotate when needed.
  2. Are you a person connecting an MCP client (Claude Desktop / VS Code) interactively? Walk the OAuth flow from the client; let it mint a UserApiKey on your behalf. Manage / revoke through the portal.
  3. Are you a person writing a script or CLI tool that calls Hadron directly? Mint a UserApiKey through the portal. Paste the raw key into your script's env or secret manager. Treat it like any other API key.
  4. Are you building something headless / IoT (no user, no browser)? Use an AppKey. The OAuth flow requires a browser for the consent step; AppKeys don't.

If you find yourself wanting a UserApiKey for a server-to-server integration "because it's easier to mint," stop and reach for an AppKey instead — the principal really is the App, not you-personally, and AppKeys live with the App's lifecycle (rotation when employees leave, regeneration on suspected leak, etc.) instead of being tied to one User row.

Multi-App users + the multiple_apps_resolved error

Most users have exactly one Hadron App at any given time — minimum for using /mcp at all. For those users, the server resolves App context automatically from the User's single membership.

For users with two or more Apps (a real case as the platform grows), the /mcp endpoint can't pick which App the request is operating on without more information from the client. The current behavior is to return:

HTTP/1.1 409 Conflict
{
  "error": "multiple_apps_resolved",
  "error_description": "User has more than one Hadron App; this MCP client does not support App selection. See https://hadronmemory.com/docs/auth/multiple-apps-resolved for guidance."
}

(The URL is the server's default; production deployments can override it via the HADRON_MULTIPLE_APPS_DOCS_URL env var so the error points at a deployment-specific docs page.)

This is a Hadron-namespaced error code (not in the MCP spec's vocabulary) because the MCP spec's authentication errors are token-scoped, not resource-ambiguity-scoped. It marks a real product gap — current MCP clients (Claude Desktop, VS Code, etc.) don't yet have a UI for "pick which Hadron App this connector represents." The solution is tracked as a sibling spec (02X-app-identification-regimes) — three planned regimes: project-directory-aware (.hadron/config.json for Claude Code/Cursor), session-scoped MCP-tool-based (h-open-app meal-planner for Claude Desktop), and the current single-App-default for the easy case.

If you hit this error today, the workaround is to use an AppKey from the specific App you want to operate on — AppKeys carry the App identity explicitly so there's no ambiguity to resolve.

See also