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.