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 project-bound private ticket credential.

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 | | ------- | ----------- | | Portal UI | Session cookie | | MCP (/api/mcp-bot) | OAuth only | | MCP tools list (/api/mcp-bot/tools) | Session or Bearer | | Context Capture ticket API | Project-bound ah_cc_* credential |

| 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.