How the CentraKit API authenticates browser sessions and bearer tokens, and how clients get tokens through Supabase OAuth 2.1, PKCE and dynamic client registration.
The CentraKit API trusts one identity provider: Supabase Auth. A request is authenticated either by a Supabase session cookie (browsers) or by a Supabase access token sent as a bearer token (scripts, the CLI and MCP clients). Both paths end in the same request context, so permissions, Row Level Security, audit logging and errors behave the same way.
The SaaS app signs users in with Supabase and stores the session in the sb-auth-token cookie. Supabase splits large sessions into chunked cookies such as sb-auth-token.0. When a request to the API has no Authorization: Bearer header, the API reads this cookie session.
The API is the /api path on each brand's app host, so the browser sends the cookie the SaaS app set on that host. Cookies are host-only: a session on app.centrakit.com does not reach app.mechup.com. Cookies suit same-site browser flows. Use bearer tokens for everything else.
Send a Supabase access token (a JWT) in the Authorization header:
GET /api/v1/users/me HTTP/1.1
Host: app.centrakit.com
Authorization: Bearer <access-token>Before the route runs, the API verifies:
https://<project-ref>.supabase.co/auth/v1/.well-known/jwks.json;https://<project-ref>.supabase.co/auth/v1;authenticated, the MCP resource URL of a brand host such as https://app.centrakit.com/mcp, or the MCP_RESOURCE_AUDIENCE value a deployment configures.As soon as the request has a Bearer authorization scheme, the cookie is ignored. An invalid or expired token gets 401 with an AUTH_INVALID_TOKEN problem and a WWW-Authenticate header that points to the protected resource metadata. The API does not fall back to the cookie session.
Supabase access tokens usually carry no CentraKit scopes. When a token does carry mcp:read or mcp:write in its scope claim, the scopes limit what the token may do. mcp:read covers the permissions of the MCP read tools and mcp:write those of the MCP mutation tools, on the REST API as on the MCP server. A few operations name their own scope where one permission backs a read and a write: reading an audit export (GET .../audit-exports/{exportId}) takes mcp:read, and creating one takes mcp:write.
A route that the token's scopes do not reach answers 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="...", naming the scope that would reach it, and a PermDock problem (type https://permdock.com/problems/denied). Routes outside the organization permission model, such as the assistant chat and PATCH /v1/users/me/active-organization, each declare the scope that reaches them: reads take mcp:read and changes take mcp:write.
Scopes only narrow what a token can do: the user's organization permissions still decide every request; see MCP server.
A token that Supabase's OAuth server issued to a client carries that client's client_id. The API treats the client as an agent acting for the user: each request is decided on the user's organization permissions, limited to what the user delegated to that client.
| Client | What it may do for the user |
|---|---|
The CentraKit CLI (the client ID in the API's CENTRAKIT_OAUTH_CLIENT_ID) | Everything the user may do |
A client whose token carries mcp:read or mcp:write | What the MCP tools covered by those scopes may do |
| Any other client | Nothing |
A token from any other client that carries only identity scopes (openid, email, profile) is refused on every organization route with the 403 insufficient_scope challenge above. A missing organization permission returns the 403 ORGANIZATION_PERMISSION_DENIED problem. Session tokens from a password or Google sign-in carry no client_id and are not limited.
centrakit login runs an OAuth 2.1 authorization code flow with PKCE against Supabase:
state.https://<project-ref>.supabase.co/auth/v1/oauth/authorize in your browser with the challenge and a loopback redirect URI, http://127.0.0.1:8976/callback by default.state and exchanges the code and verifier at /auth/v1/oauth/token.GET /api/v1/users/me with the new token and stores the access and refresh tokens in session.json (file mode 600). It refreshes the access token when it expires.Browser login needs an OAuth client ID (CENTRAKIT_OAUTH_CLIENT_ID) and the Supabase URL (CENTRAKIT_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_URL). The API must know the same client ID in its own CENTRAKIT_OAUTH_CLIENT_ID; otherwise it treats the CLI as an unknown client and refuses its requests. See CLI for every setting.
The Supabase project runs an OAuth 2.1 authorization server with dynamic client registration enabled. A client that wants to call the API on behalf of a user:
authorization_servers.MCP clients do all of this automatically when you add the server.
For scripts and CI, pass an access token you obtained earlier:
CENTRAKIT_TOKEN=<access-token> centrakit orgs list
curl -H "Authorization: Bearer $CENTRAKIT_TOKEN" https://app.centrakit.com/api/v1/organizationsAccess tokens expire. Long-running jobs need to refresh them with the refresh token from the same flow.
Personal API keys can be created on the Authentication tab of your profile (pdk_ keys with read-only or read and write access, in all your organizations or in one, with an optional expiry). The REST API under /api/v1 accepts a key as a bearer token or in the x-api-key header:
curl -H "Authorization: Bearer pdk_..." https://app.centrakit.com/api/v1/organizations/<organization-id>/workflows/definitionsA request with a key runs as you, limited to the key's permissions: the database applies the same row-level security as for your session, and a read-only key cannot change data. An unknown, expired or revoked key answers 401 with a WWW-Authenticate: Bearer challenge and a problem response. Keys work only on the REST API; the MCP server accepts OAuth tokens.
A key limited to one organization answers 403 for every other organization you belong to. Organization API keys are not available yet.
The API publishes RFC 9728 protected resource metadata on every brand host. The resource is the /mcp URL of the host that served the document.
| Environment | URL |
|---|---|
| Production | https://app.centrakit.com/.well-known/oauth-protected-resource |
| Local | https://app.localhost/.well-known/oauth-protected-resource |
{
"resource": "https://app.centrakit.com/mcp",
"authorization_servers": ["https://<project-ref>.supabase.co/auth/v1"]
}The document adds scopes_supported only when the deployment sets MCP_REQUIRED_SCOPES. The endpoint answers CORS preflight requests so browser-based clients can read it.
Authentication failures are problem details with an AUTH_* code:
| Code | Status | When |
|---|---|---|
AUTH_UNAUTHENTICATED | 401 | No session and no bearer token on a route that needs one. |
AUTH_INVALID_TOKEN | 401 | The bearer token failed verification or expired. |
AUTH_INVALID_CREDENTIALS | 401 | Wrong email or password on /auth/login. |
AUTH_INSUFFICIENT_SCOPE | 403 | The token lacks a required mcp:* scope. |
AUTH_CONFIRMATION_REQUIRED | 403 | The account email is not confirmed yet. |
AUTH_VALIDATION_ERROR | 400 | The request body is invalid. |
AUTH_PROVIDER_ERROR | 502 | Supabase Auth failed. |
AUTH_RESOURCE_SERVER_MISCONFIGURED | 500 | The deployment's audience or issuer configuration is wrong. |
A valid user without the organization permission for a route gets 403 with ORGANIZATION_PERMISSION_DENIED. See Errors.
Last updated on