Skip to content

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/login

This endpoint has two modes:

  • Web (default): reads { provider, redirectTo } from the request body. The provider-generated CSRF state is stored in an encrypted cookie for validation on callback.
  • CLI: reads provider, redirect_uri, and state from query params. The CLI manages its own CSRF state, so no cookie is set.

Request Body (Web)

FieldTypeRequiredDescription
provider'github' | 'google'YesOAuth provider
redirectTostringNoPost-auth redirect path (default: /auth/callback)

Query Parameters (CLI)

ParamTypeRequiredDescription
provider'github' | 'google'YesOAuth provider
redirect_uristringYesLocalhost callback. Must be http://127.0.0.1 or http://localhost, port 9876-9899, path /callback.
statestringNoCaller-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

json
{
  "url": "https://github.com/login/oauth/authorize?client_id=...",
  "state": "csrf-state-token"
}

Example

bash
curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"provider": "github"}'

Error Codes

StatusCondition
400Invalid provider, or (CLI) redirect_uri is not an allowed localhost callback
429Rate 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/verify

This 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/refresh when they expire.

Request Body

FieldTypeRequiredDescription
codestringConditionalOAuth authorization code (code flow)
statestringConditionalCSRF state token (required with code in web mode; CLI manages its own state)
accessTokenstringConditionalAccess token (magic link / implicit flow)
refreshTokenstringNoRefresh token (magic link / implicit flow)
source'cli'NoWhen set to 'cli', returns tokens instead of setting a cookie

Either code (+ state in web mode) OR accessToken must be provided.

Response (Web)

json
{
  "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)

json
{
  "user": { "id": "uuid", "email": "[email protected]", "provider": "github", "providerAccountId": "12345", "avatarUrl": null },
  "tokens": {
    "accessToken": "...",
    "refreshToken": "...",
    "expiresAt": 1735689600
  }
}

Error Codes

StatusCondition
400Neither code nor accessToken provided
403Invalid or missing CSRF state token (web code flow)
429Rate 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/refresh

Request Body

FieldTypeRequiredDescription
refreshTokenstringYesRefresh token from a prior verify (CLI) response

Response

json
{
  "accessToken": "...",
  "refreshToken": "...",
  "expiresAt": 1735689600
}

Error Codes

StatusCondition
400refreshToken missing
401Session expired / refresh not possible
429Rate limit exceeded

Auth

Public endpoint. Rate limited: 10 requests per minute per IP.


Send a passwordless magic link email.

POST /api/auth/magic-link

Request Body

FieldTypeRequiredDescription
emailstringYesEmail address to send the magic link to
redirectTostringNoPost-auth redirect path (default: /auth/callback)

Response

json
{ "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/me

Response

json
{
  "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/logout

Response

json
{ "ok": true }

Auth

Requires valid session.


Update Profile

Update the authenticated user's display name and/or theme.

PATCH /api/profile

Request Body

FieldTypeRequiredDescription
displayNamestringNoDisplay name. Trimmed; must be 1-100 characters.
theme'light' | 'dark' | 'system'NoUI theme preference.

At least one field must be provided.

Response

Returns the updated profile row.

Error Codes

StatusCondition
400Invalid 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/avatar

Request

multipart/form-data with a file field. Allowed types: image/jpeg, image/png, image/webp, image/gif. Maximum size: 2 MB.

Response

json
{ "avatarUrl": "data:image/webp;base64,..." }

Error Codes

StatusCondition
400Missing 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/avatar

Response

json
{ "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-workspaces

Response

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/profile

Response

json
{ "deleted": true }

Auth

Requires valid session.

Released under the AGPL-3.0 License.