/auth/v1/* on your project URL. When a user signs in, GoTrue mints two JWTs — a short-lived access token and a longer-lived refresh token — and returns them to your client. 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. For doing the signin flow, see Signup, signin, magic link.
The two tokens
GoTrue returns this shape after every successful sign-in / token refresh: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 + a new refresh token, and invalidates the old refresh token.
Most client SDKs (e.g., supabase-js) handle the refresh dance automatically — 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:sub— the user’s UUID. Returned byauth.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. DrivesSET LOCAL ROLEin PostgREST.aud— always"authenticated"on Powabase (the project provisions GoTrue withGOTRUE_JWT_AUD=authenticated).app_metadata— controlled by the platform/your backend (viaPUT /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 canPUT /auth/v1/userto change it.
auth.jwt() (returns jsonb).
Refresh token rotation
Powabase has refresh token rotation enabled by default (GOTRUE_SECURITY_REFRESH_TOKEN_ROTATION_ENABLED=true). Three implications:
- Each refresh token is single-use. Exchange it once; the next attempt with the same token returns
400 invalid_grant. - 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 — re-authentication required.
- 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 common “double-refresh” race.
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 for the longer treatment of how policies use these roles.
Email verification and the “autoconfirm” default
Powabase projects ship withGOTRUE_MAILER_AUTOCONFIRM=true by default — 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 — 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. 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 |
429 over_email_send_rate_limit (or similar). These are per-project and tunable in Helm overrides. For self-hosted deployments expecting traffic spikes, raise them; 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 audit flagged this; the platform ships with
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false. If you want anonymous-then-upgrade flows, set it totruein 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 + Twilio creds if you want phone signup. - No CAPTCHA by default.
GOTRUE_SECURITY_CAPTCHA_ENABLED=false. The hcaptcha provider is wired up; supplysecurityCaptchaSecretto enable. - No password requirements enforced beyond GoTrue defaults. That’s at least 6 characters — there’s 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
Signup, signin, magic link
End-to-end email/password and magic-link flows in three languages.
OAuth providers
Wire up Google, GitHub, and the 20 other providers with PKCE.
Auth Reference
Full /auth/v1/* endpoint catalog.
RLS Model
How the four roles compose with RLS policies on your tables.