Skip to main content
These flows assume you’ve read Auth model and have your Anon (Publishable) Key from the Connect modal. All calls here use the Anon Key as the apikey header; that’s what makes the auth endpoints reachable from public clients. After sign-in, you send the user’s access token as the Authorization: Bearer <token> header on subsequent API calls (and keep the Anon Key in apikey). For the conceptual underpinning, see Auth model. For the full endpoint surface, see Auth Reference. For OAuth flows specifically, see OAuth providers.

Headers used throughout

Every request below uses these two headers (the auth API doesn’t need its own access token; that’s what you’re trying to get):
After sign-in, replace the Authorization value with the user’s access_token. Keep apikey as the Anon Key; PostgREST and GoTrue both check it for upstream routing.

Email + password: signup

POST /auth/v1/signup creates a new user with email + password and, on default settings (autoConfirm: true), returns a session immediately. On projects where you’ve enabled email confirmation, the response is just { user } and the user has to click the email link before they can sign in.
Optional fields you can include in the body:
  • data: { ... } — populates user_metadata. Use for display name, signup source, marketing consent, etc.
  • phone: "+1..." — phone-based signup (requires GOTRUE_EXTERNAL_PHONE_ENABLED=true + Twilio configured).
  • gotrue_meta_security: { captcha_token: "..." } — required if CAPTCHA is enabled on the project.

Email + password: signin

POST /auth/v1/token?grant_type=password exchanges credentials for an access token + refresh token.
Common error responses:
  • 400 invalid_grant: wrong email/password combination, or the user doesn’t exist.
  • 400 email_not_confirmed: autoConfirm is off and the user hasn’t clicked the verification email yet.
  • 400 over_email_send_rate_limit: too many recent failed signin attempts from this IP (30/hour by default).
POST /auth/v1/otp sends an email containing a magic link. The user clicks it, GoTrue verifies the token in the link, redirects to your app with a code in the URL, and your app calls POST /auth/v1/token?grant_type=pkce to exchange that code for the session. The link flow takes two steps in your app: (1) request the magic link, (2) handle the redirect callback.
Two things to know:
  • create_user: false lets you implement “magic link only for existing users” (returns 400 if no user with that email).
  • Your redirect URL must be in the project’s URI allow list (gotrue.uriAllowList in Helm overrides, or the auth settings in the Studio). GoTrue won’t redirect to arbitrary URLs.

Password recovery

The recovery flow looks just like magic link. Request a recovery email, the user clicks it, and your callback grabs tokens from the URL fragment.
The POST /auth/v1/recover always returns 200, even for emails that don’t exist. That’s intentional: it stops an attacker from enumerating user accounts via the recovery endpoint.

Refreshing the access token

Access tokens last 1 hour. Before yours expires, exchange the refresh token for a new pair:
Rotation is on by default. Each refresh token is single-use; the response includes a fresh refresh token that must replace the old one in storage. The 10-second grace window means concurrent refresh attempts from the same client (a common SPA race) don’t both fail. Schedule the refresh before expiry, not after. A common pattern is to schedule a refresh at expires_at - 60s. If you wait for expiry, you’ll get 401s on in-flight requests.

Sign out

POST /auth/v1/logout (with the user’s access token) invalidates the refresh token server-side and emits a session-revocation event. Always clear client-side storage too. Server-side invalidation alone won’t log the user out if the access token is still in localStorage.
The scope query parameter can be global (default, revoke refresh tokens on all devices), local (just this session), or others (everywhere except this device).

Storing tokens

A short detour, because this trips people up. Where you store the tokens depends on your environment:
  • Browser SPA: localStorage is fine for most apps. The Anon Key is already public, the access token is short-lived, and the refresh token’s single-use rotation limits damage from XSS-stolen tokens. If XSS is a serious concern, use an httpOnly cookie set by your backend proxy.
  • Server-side rendering (Next.js, Remix, etc.): use httpOnly cookies set by the server. The client never sees the tokens; your server attaches them on outbound API calls.
  • Mobile (iOS, Android): the OS-native secure storage (Keychain on iOS, EncryptedSharedPreferences on Android). Don’t put refresh tokens in regular preferences.
  • CLI / desktop apps: write to a ~/.config/your-app/credentials.json with 0600 perms.
In every case, treat the refresh token as the more sensitive credential. It lasts indefinitely until rotated, while the access token expires in an hour.

Next steps

OAuth providers

Google, GitHub, and 20 more OAuth providers with PKCE.

Auth model

What’s inside the JWTs and how rotation works under the hood.

Auth Reference

Full /auth/v1/* endpoint surface.

RLS Cookbook

The policy patterns that gate what signed-in users can see.