Skip to content

Authentication API

All endpoints below operate on a collection of type: "auth". Calls against non-auth collections return 422.

For the conceptual overview see Authentication.

POST /api/admin/setup
{ "email": "...", "password": "..." }

One-time setup — creates the first admin. After that, returns 400.

POST /api/admin/auth/login
{ "email": "...", "password": "..." }
→ { "data": { "token": "<jwt>", "admin": { "id": "...", "email": "..." } } }
GET /api/admin/auth/me
→ { "data": <jwt payload> }
POST /api/auth/<col>/register
{ "email": "...", "password": "...", ...extra fields go to `data` blob }
POST /api/auth/<col>/login
{ "email": "...", "password": "..." }

/login returns one of:

{ "data": { "token": "<jwt>", "record": { "id": "...", "email": "..." } } }
{ "data": { "mfa_required": true, "mfa_token": "..." } }
POST /api/auth/<col>/login/mfa
{ "mfa_token": "...", "code": "<6 digits>" }
# OR — single-use recovery code instead of TOTP code
POST /api/auth/<col>/login/mfa
{ "mfa_token": "...", "recovery_code": "a1b2c3d4" }
GET /api/auth/me ← user JWT
POST /api/auth/refresh ← user OR admin JWT, re-issues 7d token
POST /api/auth/<col>/request-verify ← auth required, no body
POST /api/auth/<col>/verify-email { "token": "..." }

Verification emails are sent via the verify template (Settings → Email templates). The token is the long random hex from the email link.

POST /api/auth/<col>/request-password-reset { "email": "..." }
→ always 200 (no enumeration), even if email isn't registered
POST /api/auth/<col>/confirm-password-reset { "token": "...", "password": "..." }

password must be ≥ 8 characters. reset template, 1-hour token TTL.

Gated by Settings → Auth features → OTP / magic link.

POST /api/auth/<col>/otp/request { "email": "..." } ← always 200
POST /api/auth/<col>/otp/auth { "token": "..." }
POST /api/auth/<col>/otp/auth { "email": "...", "code": "<6 digits>" }

Either token (from the link) or code (from the email) authenticates. 10-minute expiry. SMTP must be configured.

Gated by Settings → Auth features → MFA / TOTP.

POST /api/auth/<col>/totp/setup ← user JWT
→ { "data": { "secret": "<base32>", "otpauth_url": "otpauth://..." } }
POST /api/auth/<col>/totp/confirm { "code": "<6 digits>" }
→ enables MFA on this user
POST /api/auth/<col>/totp/disable { "code": "<6 digits>" }
→ clears MFA on this user

disable is intentionally not gated by the feature flag — once a user has MFA enabled, disabling it shouldn’t break when an admin turns the feature off globally. Same for /login/mfa. /disable also wipes any stored recovery codes for the user.

POST /api/auth/<col>/totp/recovery/regenerate ← user JWT
→ { "data": { "codes": ["a1b2c3d4", "e5f6g7h8", ...] } } // 10 codes, plaintext, ONCE
GET /api/auth/<col>/totp/recovery/status ← user JWT
→ { "data": { "total": 10, "remaining": 7 } }

10 single-use 8-character codes per user, bcrypt-hashed at rest. Plaintext is returned once at generation time — there’s no way to retrieve them again. Regenerating invalidates the previous batch.

GET /api/auth/<col>/oauth2/providers
→ { "data": [ { "name": "google", "displayName": "Google" }, ... ] }
GET /api/auth/<col>/oauth2/authorize
?provider=google&redirectUri=https://app.example.com/callback&state=<csrf>
→ { "data": { "authorize_url": "https://accounts.google.com/o/oauth2/v2/auth?..." } }
POST /api/auth/<col>/oauth2/exchange
{ "provider": "google", "code": "...", "redirectUri": "..." }
→ { "data": { "token": "<jwt>", "record": { "id": "...", "email": "..." } } }

Configure providers at Settings → OAuth2 (13 supported out of the box).

POST /api/auth/<col>/anonymous
→ { "data": { "token": "<jwt>", "record": { "id": "...", "email": "anon_xxx@anonymous.invalid", "anonymous": true } } }

30-day JWT carrying anonymous: true claim.

POST /api/auth/<col>/promote ← anonymous user JWT
{ "email": "alice@x.com", "password": "secret123" }
→ { "data": { "token": "<jwt>", "record": { ... "anonymous": false } } }

Upgrades an existing anonymous record in place — keeps the user id, sets the email + password, clears is_anonymous, and re-issues a fresh non-anonymous JWT. Returns 403 if the caller’s token isn’t anonymous, 409 if the email is already taken in this collection.

POST /api/admin/impersonate/<col>/<userId> ← admin auth required
→ { "data": { "token": "<jwt>", "record": { ... }, "impersonated_by": "<admin_id>" } }

1-hour user JWT carrying impersonated_by: <admin_id> for audit.

GET /api/admin/users/<col>?page=1&perPage=30
PATCH /api/admin/users/<col>/<id> { email?, verified?, mfa_enabled?: false, data? }
DELETE /api/admin/users/<col>/<id>

mfa_enabled: true is rejected — admins can only disable MFA (account recovery); enabling requires the user’s own enrollment via /totp/setup + /totp/confirm.

User JWT (audience "user"):

{
"iat": 1730000000,
"exp": 1730604800,
"aud": "user",
"id": "<user_id>",
"email": "...",
"collection": "<col_name>",
"anonymous": true, // optional
"impersonated_by": "<admin_id>" // optional
}

Admin JWT (audience "admin"):

{
"iat": 1730000000,
"exp": 1730604800,
"aud": "admin",
"id": "<admin_id>",
"email": "..."
}