# Sign-in for your app's users

Your app's users sign up and sign in at `/auth/*` on your app's own address. Flatstack runs [Better Auth](https://better-auth.com) inside your project and keeps users, sessions and passwords in your project's own database, in tables named `fs_auth_*`. Your functions, routes, the data API and storage see the signed-in user as `ctx.user`, and the `owner` and `authenticated` [access rules](data.md) check it.

Every environment gets sign-in on its next deploy. Nothing to install.

## Ways to sign in

| Method | Endpoints | Notes |
| --- | --- | --- |
| Email and password | `POST /auth/sign-up/email`, `POST /auth/sign-in/email` | Passwords are hashed with scrypt. 10 wrong passwords in a row lock password sign-in for 30 minutes (below). |
| Magic link | `POST /auth/sign-in/magic-link` | Emails a link that works once, for 60 minutes. |
| One-time code | `POST /auth/email-otp/send-verification-otp`, `POST /auth/sign-in/email-otp` | A 6-digit code, for 10 minutes. |
| Google, GitHub, Apple, Microsoft | `POST /auth/sign-in/social` with `{ provider }` | Add your own OAuth app's client ID and secret in Auth settings. Redirect URI: `https://<your app>/auth/callback/<provider>`. Preview environments can use Flatstack's Google and GitHub apps until you add your own. |
| Passkeys | `/auth/passkey/*` | For your app's own hostname. |

Turn methods on and off, require email verification, and add providers in the dashboard (**Project → Auth settings**), with `flatstack auth set` and `flatstack auth providers set` ([CLI](cli.md)), with the MCP tools `get_auth_settings` and `set_auth_settings` ([MCP](mcp.md)), or with `PUT /v1/projects/:projectId/auth/settings` ([API](../api/end-user-auth.md)). Changes reach your app on its next deploy, like environment variables.

`GET /auth/fs/config` tells your sign-in screen which methods and providers are on, and the Turnstile site key. It never includes a secret.

### Sessions

Sign-in sets an HTTP-only cookie on your app's domain. Sessions last 30 days and extend when used, once a day. `POST /auth/sign-out` ends one.

Mobile apps can't use cookies well, so:

1. Sign in as usual. The response has a `set-auth-token` header: the session token.
2. `GET /auth/token` with `Authorization: Bearer <session token>` returns `{ token }`, a JWT that works for 15 minutes.
3. Call your functions with `Authorization: Bearer <JWT>`. Get a new one from `/auth/token` when it expires.

The JWT's keys are at `GET /auth/jwks` if another service needs to check it.

### Email verification, password reset, account changes

| Action | Endpoint |
| --- | --- |
| Resend verification | `POST /auth/send-verification-email` |
| Ask for a reset link | `POST /auth/request-password-reset` with `{ email, redirectTo }` |
| Set the new password | `POST /auth/reset-password` with `{ token, newPassword }` |
| Change email | `POST /auth/change-email` |
| Change password | `POST /auth/change-password` |
| Delete own account | `POST /auth/delete-user` |

Reset links land on your app's reset page (`/reset-password` unless you set another) with `?token=`. When verification is required, sign-in before it answers `403 email_not_verified`.

Auth emails come from your project's [email sender](email.md) and keep sending up to 110% of your email limit. Replace any of them with one of your own templates in Auth settings (`verification`, `passwordReset`, `magicLink`, `otp`, `changeEmail`, `invitation`); the template gets `url` (or `otp`) and `appName`.

## Roles and organisations

Every user has a role: `user`, unless you give them another. Declare your own roles in `flatstack/auth.ts`:

```ts
import { defineAuth } from "@flatstackdev/server/auth";

export const auth = defineAuth({
  roles: ["editor", "billing"],
  hooks: {
    afterSignUp: async ({ user, ctx }) => {
      await ctx.db.insert(profiles).values({ userId: user.id });
    },
  },
});
```

`admin` is built in: functions with `auth: "admin"` need it. Set roles from the dashboard, with `PATCH /v1/projects/:projectId/auth/users/:userId`, or from your app's own admin screens with `/auth/admin/set-role`.

Users can create organisations, invite people by email and give them a role in each: `owner`, `admin` or `member` (`/auth/organization/*`). The user's active organisation is `ctx.org` (`{ id, slug, name, role }`, or `null`), and access rules get it too:

```ts
export const notes = table("notes", { id: text().primaryKey(), orgId: text().notNull(), body: text() }, {
  access: {
    read: ({ org }, row) => org !== null && row.orgId === org.id,
    write: ({ org }, row) => org?.role !== "member" && row.orgId === org?.id,
  },
});
```

Turn off creating organisations with `defineAuth({ organizations: { allowCreation: false } })`.

## Limits and protection

| What | Limit | What happens |
| --- | --- | --- |
| Wrong passwords for one account | From the 3rd in a row, each try waits twice as long (1 s, 2 s, 4 s…). The 10th locks password sign-in for 30 minutes. | `429 sign_in_backoff` or `429 sign_in_locked`, with `Retry-After`. A password reset or an admin's **Unlock** clears it. Magic links, codes and passkeys still work. |
| Sign-in attempts per IP address | 10 a minute | `429 rate_limited` |
| Sign-ups per IP address | 5 a minute | `429 rate_limited` |
| Emails per IP address (links, codes, resets) | 3 a minute | `429 rate_limited` |
| Monthly active users | Your plan's MAU limit | At 100%, new sign-ups by any method get `403 mau_capped`. Existing users keep signing in. |

A user counts towards MAU once per billing period, the first time they sign in or their session refreshes. Flatstack is told only a hash of your project and user ID, never who they are.

`/auth/*` keeps working when your account is at its request limit, read-only after a failed payment, or cancelled and in its grace period, so your users can still sign in and see their data.

### Bot protection

Turn on Cloudflare Turnstile in Auth settings with your site key and secret key. Sign-up, password sign-in, magic links and one-time codes then need the widget's token in an `x-captcha-response` header, and answer `403 bot_check_failed` without a valid one.

## Admin

From **Project → Users** in the dashboard, or the [API](../api/end-user-auth.md): find users by email, name or ID; ban and unban (a ban signs them out everywhere); sign a user out of every device; unlock password sign-in; send a password reset email; change their role.

**Impersonate** opens your app signed in as the user, for an hour, so you can see what they see. Only a signed-in owner or admin can do it. The link works once, for 5 minutes, and is in the audit log. While it lasts, every page your app renders through Flatstack has a banner saying who you're signed in as, with **Stop impersonating**. Static pages get the banner from `/auth/fs/banner.js`. Your app's own admins can't impersonate users from inside the app: `/auth/admin/impersonate-user` answers `403`.

## Users imported from elsewhere

Users brought over from Supabase and similar services keep their passwords: bcrypt hashes are checked as they are, and replaced with scrypt the first time each user signs in.

## Errors

Every `/auth` error uses Flatstack's shape: `{ "error": { "code", "message", "docsUrl", "details"? } }`.

| Code | Status | When |
| --- | --- | --- |
| `invalid_credentials` | 401 | Wrong email or password. |
| `email_not_verified` | 403 | Verification is required and the email isn't verified. |
| `email_taken` | 409 | Signing up with an email that already has an account. |
| `invalid_token` | 400 | An expired or used link or code. |
| `sign_in_backoff`, `sign_in_locked` | 429 | Failed passwords (above). |
| `rate_limited` | 429 | Over a per-IP limit. |
| `bot_check_failed` | 403 | Turnstile is on and the token is missing or wrong. |
| `user_banned` | 403 | The user is banned. |
| `mau_capped` | 403 | A new sign-up at 100% of MAU. |
| `auth_not_deployed` | 501 | The environment hasn't been deployed since sign-in arrived. Deploy it again. |

Other Better Auth errors keep their message, with Better Auth's own code in `details.reason`.

## Local development

`flatstack dev` serves `/auth/*` as a deployed environment does. It creates the `fs_auth_*` tables in the local database before your migrations, runs your roles and hooks from `flatstack/auth.ts`, and uses the settings a new project starts with: email and password, magic links, one-time codes and passkeys on, and verification optional. Dashboard settings, such as OAuth providers and Turnstile, apply to deployed environments only. The key that signs local sessions is kept in `.flatstack/dev/auth-secret`.

It captures auth emails instead of sending them. `GET /auth/fs/dev/outbox?to=<email>` lists them, newest first, for an hour. Deployed environments don't have this.