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, orturnstile: null, turns it off. - To change a client ID without retyping the secret, leave
clientSecretout. A provider added for the first time needs its secret: without it the answer is400 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.
appNameappears 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_foundfrom 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