Skip to main content
FirstFlow has two credential paths, and the line between them is enforced at the /mcp endpoint. MCP clients authenticate with an OAuth access token; static API keys (ff_live_…) authenticate the REST API and the server SDK. They are not interchangeable: a static API key is deliberately rejected on /mcp, and an OAuth token only exists to authorize MCP traffic. This page explains why the split exists, how the server tells the two apart on every request, and where each credential belongs.

How the split is enforced

Every request to /mcp carries Authorization: Bearer <token>. The controller authenticates manually instead of a single guard so it can return a precise WWW-Authenticate header that tells a non-authenticated MCP client how to start the OAuth flow. It resolves the bearer token in two stages:
  1. JWT first. If the token is an in-product session JWT (the dashboard or an internal tool calling /mcp), it is accepted directly.
  2. API key second. Otherwise the token is verified as an API key. Here is the gate: the key is accepted only if it was issued by the OAuth flow — its metadata carries issuedVia: "mcp-oauth" or its name is prefixed oauth:. A key you created by hand under Settings → API keys has neither, so it is rejected with a 401.
That rejection is not a generic failure. It is a RFC 9728 401 whose WWW-Authenticate header advertises both the protected-resource metadata and the authorization-server URI, so a compliant client (Claude Code, Cursor, Claude Desktop) reads it and automatically starts the OAuth handshake instead of giving up.
The diagram below shows the full decision path inside the /mcp handler. The practical consequence: you never paste an API key into an MCP client config. You point the client at the /mcp URL and approve a consent screen. The token the client ends up using is minted by FirstFlow, lives only in the client’s secure store, and carries the oauth: marker the gate looks for.

Why MCP uses OAuth, not a static key

A static API key is a long-lived secret with a fixed scope set. That model fits a backend you control, but it is a poor fit for a coding agent: the key would sit in a config file on a developer laptop, every teammate would share or re-create one, and revoking access would mean rotating a secret across machines. OAuth + PKCE replaces that with a per-client, per-user, revocable grant. The user signs in, picks the organization, and approves a scoped consent screen, so the resulting token is tied to that person’s organization role and can be revoked from the dashboard without touching anyone else. Scope is not chosen by the client it is derived server-side from the caller’s role (admins get the full catalog; members get read and list only). See Scopes & roles for the exact mapping and how legacy coarse scopes expand.

Where each credential belongs

Use this to decide which credential a given integration needs. Static API keys are what you reach for when your own backend talks to FirstFlow:
  • the server SDK reads it from FIRSTFLOW_API_KEY (secret, server-only never ship it to a browser),
  • direct REST calls from your backend pass it as a bearer token.
For browser code, neither credential applies: the React SDK uses a browser-safe publishableKey (pk_live_…), which is not a secret and is not accepted on /mcp or the REST API. See the provider reference for that path.
The static API key (ff_live_…) is a server secret. It grants REST and server SDK access for your whole organization, so keep it in server-side environment variables only. The browser SDK never needs it use publishableKey there.

Managing API keys

Create, scope, and revoke static API keys under Settings → API keys.
1

Create the key

A new key is shown in full exactly once, at creation. Copy it then — FirstFlow stores only a hash, so it cannot show you the value again.
2

Store it as a server secret

Put it in your backend’s environment as FIRSTFLOW_API_KEY. Do not commit it, and do not expose it to client-side code.
3

Revoke when rotating or off-boarding

Keys are organization-scoped and revocable. Revoking takes effect immediately for REST and server SDK calls. It does not affect MCP clients, which hold OAuth tokens revoke an MCP grant from the OAuth side instead.

Specific cases

That is expected. Static keys are rejected on /mcp. Remove the key from the client config and connect via the URL instead claude mcp add --transport http firstflow <BACKEND_PUBLIC_URL>/mcp then approve the consent screen when the client opens it. The 401 you saw carried the OAuth metadata that triggers that flow automatically.
In-product callers (the dashboard, internal tools) present a session JWT, which the controller accepts in its first authentication stage. That path exists for first-party surfaces only; external MCP clients always go through OAuth.
The 401 advertises an issuer derived from OAUTH_ISSUER if set, otherwise BACKEND_PUBLIC_URL. Set BACKEND_PUBLIC_URL to the backend’s public origin and FRONTEND_URL to the dashboard so the consent redirect resolves. See the self-hosting configuration for the full environment reference.

Next

Connect a coding agent through the OAuth flow in the MCP quickstart, review how permissions map to roles in scopes & roles, or wire a static key into your backend with the server SDK configuration.