TrademarkTrademark
Ctrl k
Search docs...
Slices

Auth

Supabase sign-in, profiles and route gating, with a demo mode that needs no keys.
The auth slice is the Supabase layer: email and password sign-in, a profile per user, the session refresh that runs on every request, and an Upstash rate limiter in front of it. No surface talks to Supabase without it, so a composition that never needs an account (docs, store, landing) ships none of this code and none of its dependencies. You rarely name it yourself. dashboard requires orgs, and orgs requires auth, so the scaffolder pulls it in transitively.
  • src/lib/sb/ — browser, server and service-role clients, the config checks, safeNextPath.
  • src/components/UserProvider.tsx — useUser(): profile, email, global role, refresh.
  • src/app/(auth)/ — /auth (log in, sign up, forgot password) and /auth/update-password.
  • src/app/api/profile/ — GET and PATCH the profile, POST /api/profile/avatar.
  • src/proxy.ts — rate limiting, the session refresh and route gating.
  • supabase/init.sql — profiles, roles, user_global_roles, the sign-up trigger, the profiles bucket.
It adds four dependencies: @supabase/ssr, @supabase/supabase-js, @upstash/ratelimit and @upstash/redis. The older NEXT_PUBLIC_SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY names are still read, so an existing .env keeps working. Run supabase/init.sql in the project's SQL editor first; it is idempotent. Every variable is optional. Without the Supabase URL and publishable key, isSupabaseConfigured() is false and the app runs in demo mode: nothing is gated, /auth explains that authentication is disabled and offers the demo, UserProvider starts settled with no user, and the profile routes answer 503 not_configured instead of throwing. Without the two Upstash variables there is simply no rate limiting. A composition has to build and run with zero environment, so every cloud code path goes through these checks. src/proxy.ts runs on everything except static assets. It rate limits first, then calls updateSession in src/lib/sb/middleware.ts, which refreshes the session cookie and redirects a signed-out visitor away from the protected prefixes, /app and /profile:
/app/acme/settings  ->  /auth?next=%2Fapp%2Facme%2Fsettings
The forms send the user back to next after signing in, through safeNextPath, which only honours a plain same-site path so the sign-in page cannot become an open redirect. With no next, sign-in lands on /app, or on / in a composition without the dashboard. The proxy answers one question: is there a session. Whether that session belongs in a particular organization needs the database, and is the job of the organization guards. The rate limit is a sliding window of 30 requests a minute per IP and path. A refusal is a 429 in the same { error: { code, message } } shape the route handlers use, with retry-after set. The trigger in init.sql creates a profiles row and a member global role for every sign-up. API routes act through requireUserAndService(), which resolves the signed-in user and returns a service-role client: RLS is enabled with no policies, so ownership is enforced in the route by filtering on the user's id. The global role (admin, moderator, member…) is about the person across the whole app, not about any team. "Forgot password" calls resetPasswordForEmail with a redirect to /auth/update-password. Add that URL to the redirect URLs in the Supabase dashboard. The email signs the user in with a recovery session, so the page only has to call updateUser({ password }). The slice assumes the Supabase project belongs to this app alone. An app joining a project other products already use must not run init.sql as it is:
  • Prefix every table, bucket and function with the app's name.
  • Skip the trigger on auth.users, which would fire for every product's sign-ups, and create the per-user row lazily from the route that first needs it.
  • Drop the role tables unless the product has a real admin surface.
The scaffolder does not rewrite SQL; it prints a reminder whenever the slice is included.
Last updated October 18, 2026
Edit this page on GitHub