Authentication
How Supabase Auth, email verification, social providers, the admin role, the separate admin app and RLS work in the Next.js + Supabase starter.
This kit uses Supabase Auth for every account, including the sessions that approve agent connections and the sessions on the admin app. Nothing on this page is a second login system. For the cross-kit concepts, read the shared authentication page.
Where the configuration lives
| Concern | Location |
|---|---|
| Local Auth settings (site URL, redirect URLs, confirmations, providers) | apps/database/supabase/config.toml, [auth] section |
| Hosted Auth settings | The Supabase dashboard for your project |
| Supabase URL and keys for the apps | apps/web/.env.local and apps/admin/.env.local, see environment variables |
| Sign-in, sign-up, magic link and provider server actions | apps/web/src/data/auth/auth.ts |
| Login and sign-up pages | apps/web/src/app/(dynamic-pages)/(login-pages)/login and .../sign-up |
| Password reset and account pages | apps/web/src/app/(dynamic-pages)/(misc-login-pages)/ (forgot-password, update-password, confirm-delete-user, auth/*) |
| Supabase clients per context | apps/web/src/supabase-clients/{user,admin,anon,unkey}/ and apps/admin/src/supabase-clients/{user,admin,anon}/ |
| Web middleware chain | apps/web/src/proxy.ts and apps/web/src/middlewares/ |
| Admin app login and gate | apps/admin/src/app/login/, apps/admin/src/app/auth/{callback,confirm}/ and apps/admin/src/proxy.ts |
Email and password
Sign-up calls supabase.auth.signUp with emailRedirectTo pointing at /auth/callback. Local config.toml sets enable_confirmations = true, so a new user must open the verification email before signing in. The app shows a "check your email" card until then.
Locally, verification emails arrive in Mailpit at the MAILPIT_URL printed by supabase status --output-format json. In production, configure SMTP for the Supabase project. The web app's RESEND_API_KEY does not affect Supabase Auth emails. See emails.
Two routes complete email flows on the web app:
/auth/callbackexchanges acodefor a session and then redirects tonextor/dashboard./auth/confirmverifies atoken_hashfor email confirmation or password recovery and redirects to/dashboardor/update-password.
Password reset starts at /forgot-password and finishes at /update-password. config.toml lists exactly four local redirect URLs: /auth/callback and /auth/confirm on http://localhost:3000 (web) and on http://localhost:3002 (admin). The site_url stays on the web origin.
Magic links and social providers
signInWithMagicLinkAction uses signInWithOtp with the same callback. The login page offers it alongside the password form.
Social login is implemented server-side in signInWithProviderAction. The app's provider list is google, github and twitter (apps/web/src/utils/zod-schemas/social-providers.ts), and config.toml enables all three for local Auth with credentials read from GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, TWITTER_API_KEY and TWITTER_API_SECRET.
Provider buttons are off by default
The login and sign-up pages do not render provider buttons. The components exist (apps/web/src/components/authentication/render-providers.tsx and apps/web/src/app/(dynamic-pages)/(login-pages)/sign-up/provider-signup-form.tsx). To turn a provider on, render ProviderSignupForm, add the provider's credentials to your env, and register the callback URL in the provider's console and in Supabase Auth.
Sessions and protected routes
apps/web/src/proxy.ts runs the list in middlewares/middleware-list.ts in order for matching paths:
authMiddlewareon the protected paths frommiddlewares/paths.ts(/dashboard,/settings,/profile,/workspace,/project,/home,/user,/logoutand/onboarding). It callssupabase.auth.getClaims(), refreshes cookies and redirects to/loginwhen there is no session. Claims are parsed withuserClaimsSchemaso malformed tokens fail loudly.dashboardOnboardingMiddlewareandonboardingRedirectMiddlewaresend users to/onboardinguntilonboardingHasAcceptedTerms,onboardingHasCompletedProfileandonboardingHasCreatedWorkspaceare all set in their user metadata, and back to/dashboardafterwards.
The session is resolved once and passed along the chain. Server components, server actions and route handlers each use a matching client from src/supabase-clients/user/. The service-role client in src/supabase-clients/admin/ is server-only and is never imported into client code. There is no /app-admin route in the web app any more; app-admin is only a reserved slug in apps/web/src/constants.ts.
Becoming an application admin
The admin role is stored in two places that must agree:
- A row in
public.user_roleswithrole = 'admin'. RLS policies and the Admin MCP check this table throughis_application_admin()andmcp_live_actor(). app_metadata.user_role = 'admin'on the Auth user. The admin app'sproxy.tsand/app-adminlayout read this claim throughisSupabaseUserClaimAppAdmin(utils/is-supabase-user-app-admin.ts, present in both apps).
The trusted way to set both is the SQL function make_user_app_admin(user_id uuid) from apps/database/supabase/migrations/20240901054606_application_admin.sql. It can only run as postgres, supabase_admin or dashboard_user, so call it from the Supabase SQL editor or psql, never from the app:
select make_user_app_admin('<your-user-id>');The user then signs out and back in so the new claim is in their token. remove_app_admin_privilege_for_user(user_id) reverses it. A normal account cannot write app_metadata or user_roles; both are revoked from anon and authenticated.
Locally, seed.sql already calls make_user_app_admin for the seeded admin user testadmin@usenextbase.com. The Playwright admin setup does the same through a direct Postgres connection.
A custom_access_token_hook function also ships in the migrations, but the [auth.hook.custom_access_token] block in config.toml is commented out. The admin claim works without it because make_user_app_admin writes raw_app_meta_data directly.
The admin app
Administration is a separate Next.js app, apps/admin, that deploys on its own origin (port 3002 locally) and shares the Supabase project with the web app. See docs/admin-app.md in the repository.
/loginis an email and password form (signInWithPassword). The admin app has its own/auth/callbackand/auth/confirmhandlers and a/logoutroute;/redirects to/app-admin.apps/admin/src/proxy.tsruns on/app-admin,/feedbackand/roadmap. Without a session it redirects to/login?next=...; with a session whoseapp_metadata.user_roleis notadminit returns 403.- The
/app-adminlayout rechecks the claim and callsunauthorized()otherwise. Mutations go throughadminActionClientinapps/admin/src/lib/safe-action.ts, which verifies the admin role again before the service-role client is used. /app-adminholds users, workspaces, marketing content (blog, changelog, authors, tags), payment gateway data and settings;/feedbackand/roadmapon the admin origin are the moderation views.- Support sign-in and impersonation (
apps/admin/src/data/admin/user.tsx) callauth.admin.generateLinkand build a link to the web app's/auth/confirmusingNEXT_PUBLIC_WEB_APP_URL. The web app verifies the token and sets its own host-scoped cookies; the admin session never leaves the admin origin.
Owning a workspace does not make someone an application admin. See workspaces and the admin area.
What RLS protects
Row Level Security is enabled on every user-facing table defined in apps/database/supabase/migrations/. Policies key off auth.uid(), workspace membership in workspace_members, the workspace_member_role enum (owner, admin, member, readonly) and per-member permissions. Workspace settings are split into a member-visible table and an admin-only table.
apps/database/supabase/tests/ holds pgTap suites for user, workspace, project and marketing tables, function behaviour, security isolation, RLS enforcement and Data API grant safety. Run them with pnpm --filter database test-db while Supabase is running, and add a test whenever you add a table or policy.
The MCP credential table mcp_records is also behind RLS with all browser grants revoked; only service_role can read it. See agent connections.
Environment variables
Every variable in the Next.js + Supabase starter's env files for the web and admin apps, what it does and where the value comes from.
Agent connections
The User MCP and Admin MCP of the Next.js + Supabase starter: endpoints on the web and admin origins, tools, consent flow and where the code lives.