Supabase Auth ownership, session posture, OAuth linking, and deferred controls.
CentraKit uses Supabase Auth for user identity. Application code validates inputs, maps provider errors to domain errors, and avoids returning password material, while Supabase owns password hashing, access-token issuance, refresh-token rotation, and browser/server session cookie format.
supabase/config.toml; mirror this in hosted Supabase settings.gen_salt('bf', 12).@supabase/ssr with Supabase-compatible options.supabase.auth.signOut(). Immediate app-owned token blacklisting is deferred.withApiRoute adapter. User routes accept either a refreshed SSR cookie or an OAuth bearer; a present but invalid bearer never falls back to cookies. Cron and provider-webhook policies retain their own credential contracts.@supabase/server after credential normalization. Its RLS-scoped Supabase client is always safe by default, supabaseAdmin stays lazy and request-memoized, and direct Postgres clients are available only on routes that explicitly opt into postgres: "scoped" | "admin" | "both".Use supabase.auth.getClaims() for server-side checks that only need verified identity claims, such as authenticated state, current user ID (sub), current email, session ID, or coarse role hints. Use supabase.auth.getUser() only when a flow needs the fresh Auth user record, linked identities/providers, email confirmation state, or other Auth-server metadata. Use getSession() only when raw access or refresh tokens are needed.
Hosted Supabase projects should use the new JWT signing keys system with an asymmetric key, preferably ES256. getClaims() can then verify tokens with cached JWKS/WebCrypto instead of putting the Auth server in the hot path. Before rotating away from the legacy JWT secret, confirm no app, API, Edge Function, or integration verifies tokens with the legacy shared secret directly. After rotation, wait at least one access-token lifetime plus safety margin before revoking the previously used legacy key; local config uses a 15-minute JWT lifetime.
public.custom_access_token_hook enriches access tokens with system_roles and organization_roles for low-cardinality UI and proxy hints. Keep the hook lean because it runs when Supabase issues or refreshes tokens, claims are stale until the next token refresh, and large claims can bloat SSR cookies. Do not add full permission arrays to JWTs unless token size, staleness, and security have been explicitly reviewed.
The SaaS login and register forms start Google OAuth through Supabase Auth. The hosted Supabase project owns the Google provider credentials; Vercel environment variables may mirror those values but do not configure the hosted Supabase provider by themselves.
Supabase owns account linking. When a confirmed email/password user later signs in with Google and Google returns the same verified email address, Supabase automatically links the Google identity to the existing user. Do not add a public email-existence lookup or app-owned merge path.
For users created through Google first, add email/password login only after the user has proved email ownership and has an active Supabase session. Use Supabase recovery or updateUser({ password }) through the app's existing auth service methods.
For Google Cloud OAuth clients, use the Supabase callback URL as the authorized redirect URI. Local Google OAuth uses Portless aliases under localtest.me because Google rejects .localhost subdomain callbacks.
Register this callback for local testing:
https://supabase.localtest.me:1355/auth/v1/callbackThen start the OAuth dev target:
pnpm dev:oauthSignup and other token-hash emails (confirmation, recovery, magic link) link to the SaaS app's /auth/confirm route, which calls verifyOtp({ token_hash, type }). This avoids the PKCE code exchange at /auth/callback, which requires the verifier cookie set when signUp ran and breaks when the email is opened in a different browser, profile, or in-app webview.
Locally, the confirmation template lives at supabase/templates/confirmation.html and is registered in supabase/config.toml under [auth.email.template.confirmation]. The <a> href uses {{ .RedirectTo }}&token_hash={{ .TokenHash }}&type=signup, where RedirectTo is the emailRedirectTo URL the app sets when calling signUp (https://app.localhost/auth/confirm?redirectPath=...). After editing the template or config, restart Supabase: pnpm exec supabase stop && pnpm supabase:start.
For hosted Supabase projects, mirror this in the dashboard:
https://app.<apex>).https://<prod-host>/auth/confirm (and any preview hosts you use) to Redirect URLs, alongside the existing /auth/callback entries kept for OAuth.supabase/templates/confirmation.html (adjust copy only as needed). The recovery and magic link templates still use the default /auth/callback link until those flows are migrated.OAuth must keep using /auth/callback because providers return a code, not a token_hash.
The repo does not currently maintain app-owned login counters, account lockout tables, unlock tokens, or a token blacklist. Revisit those controls if Supabase-native limits, short JWTs, and generic errors are not enough for the deployed threat model.
Use Supabase's built-in email flows for confirmation and password recovery. If a future security email is not covered by Supabase, add a small Resend boundary with RESEND_API_KEY, a verified transactional sending domain, idempotency keys, and disabled click/open tracking for sensitive transactional email.