Skip to content

Authentication — portal and MCP sessions

AppHandoff uses Supabase sessions for the first-party portal and OAuth for the MCP server. General REST API keys are retired; Context Capture has a separate private ticket credential, scoped either to one project or to a whole billing account.

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:

EndpointPurpose
GET /.well-known/oauth-protected-resourceResource metadata (RFC 9728)
GET /.well-known/oauth-authorization-serverAuthorization 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.

SurfaceAuth method
Portal UISession cookie
MCP (/mcp), including tools/listOAuth or a personal access token (ah_pat_*)
Context Capture ticket APIah_cc_* credential, project-bound or account-scoped

To check an MCP credential end to end, call bootstrap: it verifies auth, server health, and caller context in one round-trip.

HTTPCodeMeaning
401—Missing or invalid credentials
401TOKEN_EXPIREDMCP access token expired — refresh and retry
401AUTH_REQUIREDNo 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.