> ## Documentation Index
> Fetch the complete documentation index at: https://docs.powabase.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Auth model

> How GoTrue issues JWT access and refresh tokens, what's in each one, the role-based session your app sees afterwards, and how the pieces compose with PostgREST RLS.

Powabase's auth layer is [GoTrue](https://github.com/supabase/gotrue) (v2.184.0) running at `/auth/v1/*` on your project URL. When a user signs in, GoTrue mints two JWTs and returns them to your client: a short-lived access token and a longer-lived refresh token. The access token rides along with every subsequent API call as `Authorization: Bearer <token>`. The refresh token is exchanged for a new access token before the old one expires.

This page covers what's in those tokens, how the token lifecycle works (especially refresh-token rotation), and how the resulting database role drives Row Level Security. For the API surface, see [Auth Reference](/api-reference/auth). For doing the signin flow, see [Signup, signin, magic link](/guides/auth-signup-signin).

## The two tokens

GoTrue returns this shape after every successful sign-in / token refresh:

```json theme={null}
{
  "access_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 3600,
  "expires_at": 1748563200,
  "refresh_token": "v1.MzQ1Njc4OTAyMzQ1Njc4OQ",
  "user": { "id": "...", "email": "...", ... }
}
```

**Access token**: a signed JWT. Default lifetime: **1 hour** (`GOTRUE_JWT_EXP=3600`). Send it as the `Authorization` header on every API call, and as the `apikey` header (both must match). When PostgREST gets it, it verifies the signature against the project's JWT Secret, sets the database session role from the `role` claim, and stores the decoded payload as a session-local setting accessible via `auth.uid()`, `auth.jwt()`, `auth.role()`.

**Refresh token**: an opaque, single-use string (not a JWT). Default lifetime: **no expiration** as long as it gets used at least every refresh cycle, but each token can only be exchanged once. You call `POST /auth/v1/token?grant_type=refresh_token` with it before the access token expires. GoTrue returns a fresh access token plus a new refresh token, and invalidates the old one.

Most client SDKs (e.g., `supabase-js`) handle the refresh automatically, so your code just sees a continuously-valid access token. If you're writing your own client, you need to track expiry and refresh in time.

## Inside the access token

The JWT payload that PostgREST and your RLS policies see:

```json theme={null}
{
  "iss": "https://{ref}.p.powabase.ai/auth/v1",
  "sub": "11111111-1111-1111-1111-111111111111",
  "aud": "authenticated",
  "role": "authenticated",
  "exp": 1748563200,
  "iat": 1748559600,
  "email": "user@example.com",
  "phone": "",
  "app_metadata": { "provider": "email", "providers": ["email"] },
  "user_metadata": { },
  "session_id": "..."
}
```

The claims your policies will reach for:

* **`sub`**: the user's UUID. Returned by `auth.uid()` in SQL.
* **`role`**: `"authenticated"` for signed-in users, `"anon"` for unauthenticated requests using the Anon Key, `"service_role"` for the platform-issued Service Role Key. Drives `SET LOCAL ROLE` in PostgREST.
* **`aud`**: always `"authenticated"` on Powabase (the project provisions GoTrue with `GOTRUE_JWT_AUD=authenticated`).
* **`app_metadata`**: controlled by the platform or your backend (via `PUT /admin/users/{id}` with the Service Role key). Use for roles, feature flags, allowed orgs, anything end users shouldn't be able to change about themselves.
* **`user_metadata`**: controlled by the user. Use for display name, avatar URL, preferences. Don't put anything security-sensitive here; the user can `PUT /auth/v1/user` to change it.

The full payload is reachable from SQL as `auth.jwt()` (returns `jsonb`).

## Refresh token rotation

Powabase has refresh token rotation enabled by default (`GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=true`). Three implications:

1. **Each refresh token is single-use.** Exchange it once; the next attempt with the same token returns `400 invalid_grant`.
2. **The new refresh token must be persisted client-side.** If you lose it (e.g., user closes the tab before localStorage commits), the session is gone and the user has to re-authenticate.
3. **There's a 10-second reuse interval** (`GOTRUE_SECURITY_REFRESH_TOKEN_REUSE_INTERVAL=10`). If two refresh requests fire concurrently with the same token (a common race in SPAs), the second one within 10 seconds succeeds rather than 400ing. This forgives the "double-refresh" race.

Rotation is what makes refresh tokens safer than long-lived access tokens. If an attacker steals a refresh token, they can exchange it exactly once before the legitimate client tries and fails, which signals compromise. The 10-second grace window doesn't materially weaken this: the attacker still only gets one fresh access token, and the legitimate client immediately discovers the breach.

## The four roles

Powabase projects come with four pre-defined database roles that PostgREST switches between based on the JWT it receives:

| Role             | Granted to                                                                    | When                           |
| ---------------- | ----------------------------------------------------------------------------- | ------------------------------ |
| `anon`           | Anyone calling with the **Anon (Publishable) Key** as `Authorization: Bearer` | Public/unauthenticated paths   |
| `authenticated`  | Anyone calling with a **signed-in user's access token**                       | After successful sign-in       |
| `service_role`   | Anyone calling with the **Service Role (Secret) Key**                         | Server-side / trusted backends |
| `supabase_admin` | Direct Postgres connection as the project owner                               | Migrations, admin scripts      |

`anon`, `authenticated`, and `service_role` are GoTrue-issued JWTs with the corresponding `role` claim. `supabase_admin` is a database role you connect as directly, bypassing GoTrue entirely.

Both `anon` and `authenticated` respect Row Level Security. `service_role` and `supabase_admin` bypass it (`BYPASSRLS` is set on the database role definitions).

See [RLS Model](/concepts/rls-model) for the longer treatment of how policies use these roles.

## Email verification and the "autoconfirm" default

Powabase projects ship with **`GOTRUE_MAILER_AUTOCONFIRM=true`** by default, so new signups are confirmed immediately without an email verification step. That's the right setting for prototyping and for apps where you'll verify ownership another way (e.g., paid plans through Stripe). It's the wrong setting if you need to be sure users own their email address.

To turn it on, set `gotrue.autoConfirm: "false"` in your Helm overrides (self-hosted) or in the Studio's auth settings (managed), and configure SMTP credentials. After that, `POST /auth/v1/signup` returns a user record but no session, and the user has to click the verification email link before they can sign in.

SMTP is **not** configured by default. The platform won't try to send emails until you fill in `gotrue.smtpHost` and friends. While SMTP is unset, password recovery, magic links, and email verification silently no-op.

## Multi-factor authentication (TOTP)

TOTP MFA is enabled at the GoTrue level (`GOTRUE_MFA_TOTP_ENROLL_ENABLED=true` and `GOTRUE_MFA_TOTP_VERIFY_ENABLED=true`) but requires your app to drive the enrollment flow. The endpoint surface (`/auth/v1/factors`, `/auth/v1/factors/{id}/challenge`, etc.) is described in the [Auth Reference](/api-reference/auth). Up to 10 factors per user by default.

Phone-based MFA (SMS) is off by default. Turn it on and configure a Twilio account if you want it.

## Rate limits

GoTrue enforces per-IP rate limits at the application layer. The defaults Powabase ships with:

| Endpoint family                                   | Limit | Period        |
| ------------------------------------------------- | ----- | ------------- |
| Email-sending (signup, recovery, magic link)      | 30    | per hour      |
| SMS-sending                                       | 30    | per hour      |
| Token refresh (`/token?grant_type=refresh_token`) | 150   | per 5 minutes |
| Verify (signup confirm, recover confirm, etc.)    | 30    | per 5 minutes |
| OTP verify                                        | 30    | per 5 minutes |
| Anonymous user creation                           | 30    | per hour      |

Hitting a limit returns `429 over_email_send_rate_limit` (or similar). These are per-project and tunable in Helm overrides. Raise them on self-hosted deployments expecting traffic spikes; for managed-cloud projects, contact support.

## What's *not* in the picture

A few things to be explicit about because they trip people up:

* **No anonymous sign-in by default.** The platform ships with `GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false`. If you want anonymous-then-upgrade flows, set it to `true` in Helm overrides. (Most apps don't need it; "Anon Key" is the unauthenticated mode.)
* **No phone-based signup by default.** Same story: `GOTRUE_EXTERNAL_PHONE_ENABLED=false`. Enable it plus Twilio creds if you want phone signup.
* **No CAPTCHA by default.** `GOTRUE_SECURITY_CAPTCHA_ENABLED=false`. The hcaptcha provider is wired up; supply `securityCaptchaSecret` to enable.
* **No password requirements enforced beyond GoTrue defaults.** That's at least 6 characters, with no complexity rule. If you need stronger policies, validate client-side before submitting or via your own backend before calling GoTrue admin endpoints.

## Next steps

<CardGroup cols={2}>
  <Card title="Signup, signin, magic link" href="/guides/auth-signup-signin">
    End-to-end email/password and magic-link flows in three languages.
  </Card>

  <Card title="OAuth providers" href="/guides/auth-oauth-providers">
    Wire up Google, GitHub, and the 20 other providers with PKCE.
  </Card>

  <Card title="Auth Reference" href="/api-reference/auth">
    Full /auth/v1/\* endpoint catalog.
  </Card>

  <Card title="RLS Model" href="/concepts/rls-model">
    How the four roles compose with RLS policies on your tables.
  </Card>
</CardGroup>
