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.


Managed-Pair Interactive Sign-In ​

Managed pair only

These are browser redirect endpoints, not JSON APIs. They exist only on the managed auth pair and return 404 on the self-hosted (Supabase) pair, where GoTrue owns OAuth and magic links. They are the interactive counterparts to the JSON POST /api/auth/login and POST /api/auth/magic-link documented above -- the browser (or CLI) navigates to them directly and follows the redirects.

OAuth Sign-In ​

Runs the full OAuth authorization-code dance for GitHub or Google in two legs against the same URL.

GET /api/auth/oauth/:provider

:provider must be github or google.

  • Leg 1 (no code): stashes flow context in a sealed cookie, then redirects the browser to the provider's consent screen. A CSRF state cookie is generated and validated by the auth module itself.
  • Leg 2 (?code=...): the provider redirects back here; the module validates its own state, exchanges the code, links or creates the user, and then either sets the web session cookie and redirects into the app, or -- for a CLI flow -- redirects to the CLI callback with a one-time code.

Query Parameters ​

ParamTypeRequiredDescription
redirectstringNoPost-auth in-app redirect path (must start with /; default /)
cli_redirectstringNoCLI callback URL. When present, sign-in completes by redirecting here with a one-time code instead of setting a web cookie
cli_statestringNoCSRF state echoed back on the CLI callback

Errors ​

StatusCondition
400:provider is not github or google
404Not running on the managed pair

On provider-side failure the handler redirects to /auth/login?error=oauth rather than returning an error status.


Landing endpoint for a magic-link or invite email. Consumes the single-use token server-side, sets the session cookie, and 302s into the app. A server-side GET (rather than a client POST) keeps the flow working across browsers and devices.

GET /api/auth/magic/verify?token=<token>&redirect=/

Query Parameters ​

ParamTypeRequiredDescription
tokenstringYesSingle-use, 1-hour magic-link / invite token
redirectstringNoPost-auth in-app redirect path (must start with /; default /)

A missing or invalid/consumed token redirects to /auth/login?error=magic-link (no error status).

Auth ​

Public endpoint on the managed pair. Rate limited: 10 requests per minute per IP. Returns 404 on the self-hosted pair, 429 when rate limited.


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.