Skip to content

Authentication — API keys, OAuth, and MCP sessions

AppHandoff supports three authentication modes depending on which surface you use. The MCP server for AI coding agents uses OAuth only; REST integrations use API keys or Bearer JWTs.

For programmatic REST access, create an API key in the portal under Settings → API keys.

Keys use the ah_k_ prefix. Pass them on every request:

Authorization: Bearer ah_k_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Manage keys via the portal UI or programmatically with a session:

  • GET /api/keys — list keys
  • POST /api/keys — create a key
  • DELETE /api/keys/[id] — revoke a key

API keys work on most REST endpoints. They are not accepted on the MCP JSON-RPC endpoint (/api/mcp-bot).

The portal uses Supabase session cookies after login. Session auth covers portal UI routes and SSE streams (e.g. ticket realtime).

POST /api/auth/login
Content-Type: application/json
{ "email": "you@example.com", "password": "..." }

200 response:

{
"token": "<access_token>",
"refresh_token": "<refresh_token>",
"expires_in": 28800,
"user": { "id": "...", "email": "..." }
}

Use token as Authorization: Bearer <token> for API calls from your app.

POST /api/auth/send-magic-link
{ "email": "you@example.com" }

When the access token is about to expire or you receive a 401:

POST /api/auth/refresh
{ "refresh_token": "<refresh_token>" }

Returns a new token and optionally a new refresh_token.

MCP clients must use OAuth. Discovery endpoints:

| Endpoint | Purpose | | -------- | ------- | | GET /.well-known/oauth-protected-resource | Resource metadata (RFC 9728) | | GET /.well-known/oauth-authorization-server | Authorization server metadata (RFC 8414) |

The token_endpoint is on AppHandoff (/api/oauth/token), not Supabase’s host. Use exactly what discovery returns.

POST /api/oauth/token
Content-Type: application/json
{
"grant_type": "refresh_token",
"refresh_token": "<token>",
"client_id": "<uuid>"
}

Important: Do not request the offline_access scope — Supabase rejects it. Refresh tokens are issued unconditionally on the authorization code grant.

Every MCP response includes X-Token-Expires-In (seconds remaining). Refresh proactively when it drops below 300.

| Surface | Auth method | | ------- | ----------- | | REST API (integrations) | API key (ah_k_*) or Bearer JWT | | Portal UI | Session cookie | | MCP (/api/mcp-bot) | OAuth only | | MCP tools list (/api/mcp-bot/tools) | Session or Bearer | | SSE ticket stream | Session, Bearer, or nonce |

| HTTP | Code | Meaning | | ---- | ---- | ------- | | 401 | — | Missing or invalid credentials | | 401 | TOKEN_EXPIRED | MCP access token expired — refresh and retry | | 401 | AUTH_REQUIRED | No bearer token or token rejected — re-run OAuth | | 403 | — | Valid auth but insufficient access to the resource |

See Connect MCP for MCP-specific OAuth troubleshooting.