Skip to content

Your app’s users: settings and admin

Sign-in settings for a project’s app, and admin actions on its users. How sign-in works inside the app is in Sign-in for your app’s users. Everything here is also on Project → Users and Auth settings in the dashboard.

All paths are under /v1/projects/:projectId/auth. Errors use the usual shape: { "error": { "code", "message", "docsUrl", "details"? } }. Every change is in the audit log.

Settings

Method and path Permission What it does
GET /settings projects:read The settings. Secrets show only as clientSecretSet: true or secretKeySet: true.
PUT /settings projects:write Changes the fields you send; the rest stay. Reaches the app on its next deploy.
{
  "appName": "Ada's shop",
  "methods": { "emailPassword": true, "magicLink": true, "emailOtp": false, "passkeys": true },
  "emailVerification": "required",
  "providers": {
    "google": { "clientId": "123.apps.googleusercontent.com", "clientSecret": "…" },
    "microsoft": { "clientId": "…", "clientSecret": "…", "tenantId": "common" },
    "github": null
  },
  "turnstile": { "siteKey": "0x4AAA…", "secretKey": "…" },
  "trustedOrigins": ["https://www.example.com"],
  "templates": { "passwordReset": "my-reset-email" },
  "passwordResetPage": "/reset-password",
  "invitationPage": "/accept-invitation"
}
  • A provider set to null, or turnstile: null, turns it off.
  • To change a client ID without retyping the secret, leave clientSecret out. A provider added for the first time needs its secret: without it the answer is 400 validation_failed.
  • Secrets are encrypted with the project’s key and only ever sent to the app’s own Worker. The audit entry lists which fields changed, never their values.
  • appName appears in auth emails and on passkeys; it’s the project’s name until you set it.
  • Settings belong to the account that owns the project: a token for another account gets 404 not_found from both routes and changes nothing.

From the CLI and MCP

The same settings, without writing JSON:

CLI MCP Permission
flatstack auth settings get_auth_settings projects:read
flatstack auth set --email-otp off --email-verification required … set_auth_settings projects:write
flatstack auth providers set google --client-id …, flatstack auth providers remove google --yes set_auth_settings with providers projects:write
flatstack auth turnstile set --site-key …, flatstack auth turnstile remove --yes set_auth_settings with turnstile projects:write

auth providers set and auth turnstile set ask for the secret without showing it, or read it from stdin, so it stays out of your shell history; --keep-secret changes the rest and keeps the stored one. set_auth_settings takes the same body as PUT /settings, plus projectId. Neither the CLI nor any MCP tool ever prints or returns a secret. See the CLI and MCP.

Users

Each of these takes ?environment= (production unless given). Users are per environment: a preview has its own.

Method and path Permission What it does
GET /users?q=&limit=&offset= data:read Newest first. q matches email or name (any part, any case) or an exact user ID. limit is 50 by default, at most 200. Returns { users, total, limit, offset }.
GET /users/:userId data:read One user.
PATCH /users/:userId data:write { "role": "editor" }. Up to 32 lowercase letters, numbers, - and _.
POST /users/:userId/ban data:write { "reason"?, "expiresAt"? }. Signs the user out everywhere; they can’t sign in until unbanned or expiresAt.
POST /users/:userId/unban data:write
POST /users/:userId/unlock data:write Clears a lockout after 10 failed passwords.
POST /users/:userId/sign-out data:write Ends every session. Returns { sessions }, how many.
POST /users/:userId/password-reset data:write Emails the user a reset link from the project’s sender. Returns { sent: true, to }.
POST /users/:userId/impersonate data:write, signed-in owner or admin only Returns { url, expiresAt, user }.

A user looks like:

{
  "id": "Xk3…",
  "name": "Ada",
  "email": "ada@example.com",
  "emailVerified": true,
  "role": "user",
  "banned": false,
  "banReason": null,
  "banExpires": null,
  "lockedUntil": null,
  "lastActiveDay": "2026-11-03",
  "createdAt": "2026-10-29T09:52:17.474Z"
}

Impersonation

url opens the app signed in as the user, on the app’s own address. It works once, within 5 minutes; the session it starts lasts an hour and shows a banner on every page saying who you’re signed in as, with Stop impersonating. Deploy tokens and personal access tokens get 403 forbidden: impersonation needs a person, signed in to the dashboard. A banned user can’t be impersonated (409 conflict). The audit entry is end_user.impersonated, with the user’s ID.

Audit actions

auth.settings_updated, end_user.role_changed, end_user.banned, end_user.unbanned, end_user.unlocked, end_user.signed_out, end_user.password_reset_sent, end_user.impersonated.

As Markdown: api/end-user-auth.md