Auth overview
Every project has its own users, its own signing key and its own auth settings. Your app calls base.auth and the SDK handles tokens for you.
Sign-in methods
Section titled “Sign-in methods”| Method | SDK | Page |
|---|---|---|
| Email + password | auth.signUp, auth.signInWithPassword |
Email and password |
| Magic link, email OTP | auth.signInWithMagicLink, auth.signInWithOtp + auth.verifyOtp |
Magic link and OTP |
| Social providers | auth.signInWithOAuth |
Social providers |
| Enterprise SSO (OIDC, SAML) | auth.signInWithSSO |
Enterprise SSO |
| Anonymous (guest) | auth.signInAnonymously |
below |
| Invitation | auth.acceptInvite |
Admin API |
| Second factor (TOTP) | auth.mfa.* |
MFA |
Enable and configure methods in the dashboard under Authentication. A login page can call GET /auth/v1/settings to see which methods are on.
Sessions and tokens
Section titled “Sessions and tokens”| Access token | Refresh token | |
|---|---|---|
| Format | JWT signed with the project key (EdDSA) | opaque lbr_... |
| Lifetime | 600 s by default (configurable 300-3600 s) | 30 days |
| Rotation | re-issued on every refresh | rotated on every use; reuse revokes the session family |
A token from one project is rejected by every other project. Claims include sub, role, session_id, is_anonymous, aal and amr, plus tenant_id and tenant_role when tenants are enabled.
The SDK refreshes about 60 seconds before expiry, shares one refresh between concurrent callers (important because refresh tokens rotate), and signs the user out locally when the server reports a revoked or reused token.
const { data: { subscription } } = base.auth.onAuthStateChange((event, session, info) => { // INITIAL_SESSION | SIGNED_IN | TOKEN_REFRESHED | SIGNED_OUT if (event === "SIGNED_OUT" && info.reason === "refresh_token_reused") showSecurityNotice()})subscription.unsubscribe()
await base.auth.getSession() // refreshes first if neededawait base.auth.getUser() // validated by the serverawait base.auth.signOut() // this sessionawait base.auth.signOut({ scope: "global" }) // every session of the userWhere the refresh token lives
Section titled “Where the refresh token lives”The access token is always kept in memory. For the refresh token you choose:
| Mode | Option | Survives reload |
|---|---|---|
| Memory (default) | none | no |
| localStorage | auth: { storage: localStorageAdapter() } |
yes, readable by any script on the page |
| HttpOnly cookie | auth: { cookieMode: true } |
yes, recommended for same-site browser apps |
| Custom (Keychain, AsyncStorage) | auth: { storage: myAdapter } |
yes |
Cookie mode needs your app and the API to be same-site and your app origin in the project’s trusted origins. See also the Next.js and React Native packages.
Redirects
Section titled “Redirects”redirectTo must be the origin of the project’s site URL, a trusted origin, or a registered redirect URL prefix (a custom app scheme such as myapp://auth is allowed). Anything else fails with 400 redirect_not_allowed. Magic link, OAuth and SSO flows use PKCE; the SDK keeps the verifier in its storage.
Anonymous users and linking
Section titled “Anonymous users and linking”await base.auth.signInAnonymously() // is_anonymous: true in the token// later, keep the same user id and every row it owns:await base.auth.linkIdentity({ provider: "email", email, password })await base.auth.linkIdentity({ provider: "google", redirectTo })Restrict guests in policies with (select auth.jwt() ->> 'is_anonymous')::boolean. Anonymous users that are never linked are deleted after a retention period (30 days by default).
Custom claims
Section titled “Custom claims”A claims hook adds application roles or ids to the access token without a backend. Write one SQL function and point the project’s auth config hook_function at it (dashboard, or PATCH /v1/projects/{ref}/config/auth):
create function public.custom_access_token_claims(event jsonb)returns jsonb language sql stable set search_path = '' as $$ select jsonb_build_object( 'app_role', coalesce((select r.role from public.user_roles r where r.user_id = (event->>'user_id')::uuid), 'member'))$$;revoke all on function public.custom_access_token_claims(jsonb) from public;grant execute on function public.custom_access_token_claims(jsonb) to base_auth_hook;The hook runs as base_auth_hook (grant it read access to the tables it uses) with a 200 ms timeout, and must return a JSON object of at most 4 KB. Reserved claims (sub, role, aud, iss, exp, iat, session_id, project_id, is_anonymous…) are dropped. If the hook fails, no token is issued. Claims are recomputed on every refresh. Read them in policies with auth.claim('app_role').
Rate limits
Section titled “Rate limits”Sign-in, sign-up, OTP, magic link and MFA attempts are rate limited per IP, per account and per email. Limited requests answer 429 over_request_rate_limit with a Retry-After header.
Troubleshooting
Section titled “Troubleshooting”| Error | Meaning |
|---|---|
401 invalid_credentials |
wrong email or password |
429 over_request_rate_limit |
a limit or lockout applies; see Retry-After |
403 origin_not_allowed |
cookie or preflight from an origin not in the trusted origins |
403 email_not_confirmed |
email verification is required for this project |
403 user_banned |
the user is banned |
400 redirect_not_allowed |
redirectTo is not allowed |