Skip to content

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.

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.

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 needed
await base.auth.getUser() // validated by the server
await base.auth.signOut() // this session
await base.auth.signOut({ scope: "global" }) // every session of the user

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.

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.

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).

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').

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.

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