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.
API keys (REST integrations)
Section titled “API keys (REST integrations)”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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxManage keys via the portal UI or programmatically with a session:
GET /api/keys— list keysPOST /api/keys— create a keyDELETE /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).
Portal session (browser)
Section titled “Portal session (browser)”The portal uses Supabase session cookies after login. Session auth covers portal UI routes and SSE streams (e.g. ticket realtime).
Email + password
Section titled “Email + password”POST /api/auth/loginContent-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.
Magic link
Section titled “Magic link”POST /api/auth/send-magic-link{ "email": "you@example.com" }Token refresh
Section titled “Token refresh”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.
OAuth (MCP clients)
Section titled “OAuth (MCP clients)”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.
Refresh an MCP access token
Section titled “Refresh an MCP access token”POST /api/oauth/tokenContent-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.
Auth matrix
Section titled “Auth matrix”| 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 |
Common errors
Section titled “Common errors”| 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.