Authentication API
Studio uses cookie-based session authentication. The OAuth flow starts with a redirect URL, completes with code/token exchange, and stores an encrypted httpOnly cookie.
Internal API
These are internal endpoints, not a public external API. They are consumed by the Studio SPA (cookie / encrypted-session based) and the Contentrain CLI (token based, via source: 'cli'). Shapes may change without notice.
Login (OAuth Redirect)
Get an OAuth redirect URL for GitHub or Google.
POST /api/auth/loginThis endpoint has two modes:
- Web (default): reads
{ provider, redirectTo }from the request body. The provider-generated CSRFstateis stored in an encrypted cookie for validation on callback. - CLI: reads
provider,redirect_uri, andstatefrom query params. The CLI manages its own CSRF state, so no cookie is set.
Request Body (Web)
| Field | Type | Required | Description |
|---|---|---|---|
provider | 'github' | 'google' | Yes | OAuth provider |
redirectTo | string | No | Post-auth redirect path (default: /auth/callback) |
Query Parameters (CLI)
| Param | Type | Required | Description |
|---|---|---|---|
provider | 'github' | 'google' | Yes | OAuth provider |
redirect_uri | string | Yes | Localhost callback. Must be http://127.0.0.1 or http://localhost, port 9876-9899, path /callback. |
state | string | No | Caller-supplied CSRF state. Falls back to the provider-generated state when omitted. |
CLI mode activates automatically when both provider and redirect_uri query params are present.
Response
{
"url": "https://github.com/login/oauth/authorize?client_id=...",
"state": "csrf-state-token"
}Example
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"provider": "github"}'Error Codes
| Status | Condition |
|---|---|
400 | Invalid provider, or (CLI) redirect_uri is not an allowed localhost callback |
429 | Rate limit exceeded |
Auth
Public endpoint. Rate limited: 10 requests per minute per IP.
Verify (Token Exchange)
Exchange an OAuth authorization code or magic link tokens for a session.
POST /api/auth/verifyThis endpoint has two modes:
- Web (default): stores tokens in an encrypted httpOnly cookie and returns
{ user }. - CLI (
source: 'cli'): returns{ user, tokens }and does not set a cookie. The CLI stores the tokens locally and calls/api/auth/refreshwhen they expire.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Conditional | OAuth authorization code (code flow) |
state | string | Conditional | CSRF state token (required with code in web mode; CLI manages its own state) |
accessToken | string | Conditional | Access token (magic link / implicit flow) |
refreshToken | string | No | Refresh token (magic link / implicit flow) |
source | 'cli' | No | When set to 'cli', returns tokens instead of setting a cookie |
Either code (+ state in web mode) OR accessToken must be provided.
Response (Web)
{
"user": {
"id": "uuid",
"email": "[email protected]",
"avatarUrl": "https://avatars.githubusercontent.com/u/123",
"provider": "github",
"providerAccountId": "12345"
}
}The server sets an encrypted httpOnly cookie (h3-session) containing the access token, refresh token, and expiration timestamp.
Response (CLI)
{
"user": { "id": "uuid", "email": "[email protected]", "provider": "github", "providerAccountId": "12345", "avatarUrl": null },
"tokens": {
"accessToken": "...",
"refreshToken": "...",
"expiresAt": 1735689600
}
}Error Codes
| Status | Condition |
|---|---|
400 | Neither code nor accessToken provided |
403 | Invalid or missing CSRF state token (web code flow) |
429 | Rate limit exceeded |
Auth
Public endpoint. Rate limited: 10 requests per minute per IP.
Refresh (Token Refresh)
Exchange a refresh token for a new access + refresh token pair. Primarily for the CLI -- web clients are auto-refreshed by the auth middleware from the encrypted cookie.
POST /api/auth/refreshRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
refreshToken | string | Yes | Refresh token from a prior verify (CLI) response |
Response
{
"accessToken": "...",
"refreshToken": "...",
"expiresAt": 1735689600
}Error Codes
| Status | Condition |
|---|---|
400 | refreshToken missing |
401 | Session expired / refresh not possible |
429 | Rate limit exceeded |
Auth
Public endpoint. Rate limited: 10 requests per minute per IP.
Magic Link
Send a passwordless magic link email.
POST /api/auth/magic-linkRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Email address to send the magic link to |
redirectTo | string | No | Post-auth redirect path (default: /auth/callback) |
Response
{ "sent": true }Returns 200 regardless of whether the email exists (prevents enumeration).
Auth
Public endpoint. Rate limited: 5 requests per minute per IP.
Current User
Get the currently authenticated user with profile data.
GET /api/auth/meResponse
{
"user": {
"id": "uuid",
"email": "[email protected]",
"avatarUrl": "https://avatars.githubusercontent.com/u/123",
"provider": "github",
"providerAccountId": "12345",
"displayName": "Jane Doe",
"theme": "system"
}
}avatarUrl, displayName, and theme are merged from the user's profile row (theme defaults to system).
Auth
Requires valid session.
Logout
End the current session and clear the session cookie.
POST /api/auth/logoutResponse
{ "ok": true }Auth
Requires valid session.
Update Profile
Update the authenticated user's display name and/or theme.
PATCH /api/profileRequest Body
| Field | Type | Required | Description |
|---|---|---|---|
displayName | string | No | Display name. Trimmed; must be 1-100 characters. |
theme | 'light' | 'dark' | 'system' | No | UI theme preference. |
At least one field must be provided.
Response
Returns the updated profile row.
Error Codes
| Status | Condition |
|---|---|
400 | Invalid display name length, invalid theme value, or no fields to update |
Auth
Requires valid session.
Upload Avatar
Upload a custom avatar image. The image is resized to 256x256 WebP and stored as a base64 data URI on the profile.
POST /api/profile/avatarRequest
multipart/form-data with a file field. Allowed types: image/jpeg, image/png, image/webp, image/gif. Maximum size: 2 MB.
Response
{ "avatarUrl": "data:image/webp;base64,..." }Error Codes
| Status | Condition |
|---|---|
400 | Missing file, unsupported content type, or file larger than 2 MB |
Auth
Requires valid session.
Remove Avatar
Remove the custom avatar, reverting to the OAuth provider avatar.
DELETE /api/profile/avatarResponse
{ "avatarUrl": null }Auth
Requires valid session.
Owned Workspaces
List owned secondary workspaces that have other members. Used by the account deletion flow to surface ownership-transfer requirements.
GET /api/profile/owned-workspacesResponse
An array of workspace objects (each including its workspace_members). Workspaces with no members other than the owner are filtered out.
Auth
Requires valid session.
Delete Account
Permanently delete the authenticated user's account. Cleans R2 storage for every owned workspace's projects, then deletes the user from auth.users. A database CASCADE removes profiles, workspaces, members, projects, and all child records. The session cookie is cleared.
DELETE /api/profileResponse
{ "deleted": true }Auth
Requires valid session.
Related Pages
- API Reference Overview -- authentication model
- Architecture -- session management details