Nextbase Docs
Nextbase Docs
HomeBlogWelcome to Nextbase v5!Choose your starterGetting startedLocal Supabase per agent
TanStack + SupabaseInstallationEnvironment variablesAuthenticationAgent connectionsProject structureDeployment
AuthenticationWorkspaces and the admin area
Building with agentsEmails with Resend
TroubleshootingNextbase v5
The 8 starter kitsTanStack + Supabase

Authentication

How Supabase Auth, cookie sessions, application roles and row-level security protect the TanStack + Supabase starter.

This kit uses Supabase Auth for users, both apps and agent connections alike. The shared authentication page explains the concepts; this page shows where they live in this repository. Paths are given for apps/web; the admin app mirrors the same src/.server/ modules.

How a request is authenticated

The root middleware in apps/web/src/root.tsx creates one RequestServices object per request (apps/web/src/.server/request-context.ts) and places it in servicesContext. Loaders and actions call context.get(servicesContext) and get:

  • supabase(): a request-scoped @supabase/ssr server client that reads the session from cookies. Row-level security applies to every query.
  • serviceSupabase(): a service-role client that bypasses RLS. Call it only after authenticating and authorizing the caller in the same request.
  • user(): the current Supabase user, or null.

Outgoing auth cookies accumulate on the response, so session refreshes reach the browser. Sessions are Supabase cookie sessions scoped to each app's own host; the local config.toml sets jwt_expiry = 3600.

Guards live in src/.server/auth.ts of each app:

  • requireUser(context) redirects to /login?next=<current path> when there is no session.
  • requireAdmin(context) calls requireUser and then returns 403 unless user.app_metadata.user_role === "admin".

In the web app, the /app/* routes nest under routes/app-layout.tsx, which requires a user, current terms acceptance and completed onboarding. In the admin app, the /admin/* routes nest under routes/admin-layout.tsx, which requires an admin.

Sign-up, login and recovery

Web app (apps/web/src/routes.ts):

FlowRouteWhat happens
Sign up/sign-upauth.signUp with emailRedirectTo set to ${APP_URL}/auth/callback?next=.... Shows "Check your inbox to confirm your email."
Password login/loginauth.signInWithPassword, then redirects to next (default /app).
Magic link/login, "Or email me a sign-in link"auth.signInWithOtp with the same callback.
Forgot password/forgot-passwordauth.resetPasswordForEmail redirecting to /auth/callback?next=/reset-password.
Reset password/reset-passwordSets the new password for the recovered session.
Code exchange/auth/callbackauth.exchangeCodeForSession(code), then redirects to next.
Token hash/auth/confirmauth.verifyOtp({ token_hash, type }) for email or recovery links.
Logout/logoutEnds the session.

The admin app (apps/admin/src/routes.ts) has the same /login, /forgot-password, /reset-password, /auth/callback, /auth/confirm and /logout routes on its own origin, with next defaulting to /admin. It has no /sign-up: administrators are regular accounts created on the web app (or from /admin/users) and granted the admin role as described below.

Every next value passes through safeRedirectPath, so only same-site paths are accepted. If next starts with /mcp/, the app uses a full-document redirect so an agent consent request survives the login.

Email confirmation and the local inbox

Local Supabase Auth requires confirmation before sign-in (enable_confirmations = true in apps/database/supabase/config.toml). Confirmation, magic link and recovery emails are sent by Supabase Auth, not by the apps. Locally, [local_smtp] is enabled and mail lands in Mailpit; open the MAILPIT_URL printed by pnpm --filter database exec supabase status --output-format json. In production, configure SMTP in the Supabase project's Auth settings as described in emails with Resend.

The seeded accounts (testadmin@usenextbase.com, alice@example.com, bob@example.com, carol@example.com, password password123) are pre-confirmed so you can skip the inbox during development.

Redirect URLs

config.toml allows these redirect targets for local development:

site_url = "http://localhost:3000"
additional_redirect_urls = [
  "http://localhost:3002/auth/callback",
  "http://localhost:3002/auth/confirm",
  "http://localhost:3000/auth/callback",
  "http://localhost:3000/auth/confirm",
]

/auth/callback and /auth/confirm are the only auth routes registered in apps/web/src/routes.ts and apps/admin/src/routes.ts, and they are the only entries in the allowlist. In production, add the same two paths under both the web and the admin origin in the Supabase dashboard (Auth, URL Configuration) and keep Site URL on the web app's APP_URL.

Social providers

config.toml enables Google, GitHub and Twitter for the local Auth service, reading their credentials from GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, TWITTER_API_KEY and TWITTER_API_SECRET in the shell that runs the CLI.

Neither app shows provider buttons. The login and sign-up pages offer password and magic link only, and there is no signInWithOAuth call in apps/web/src or apps/admin/src. To add social login, add the provider button and auth.signInWithOAuth call to routes/login.tsx and routes/sign-up.tsx, then enable the provider and its callback URL in your Supabase project.

Becoming an application admin

Application administration is separate from workspace roles. Being a workspace owner does not open /admin.

The admin role is a row in public.user_roles with role = 'admin', mirrored into the user's app_metadata.user_role claim. Assign it with the SQL function shipped in apps/database/supabase/migrations/20240901054606_application_admin.sql:

select public.make_user_app_admin('<user uuid>');

The function refuses to run unless the current role is postgres, supabase_admin or dashboard_user, so run it from the Supabase SQL editor or a direct psql connection. It inserts the role row and writes user_role = "admin" into auth.users.raw_app_meta_data. The user must sign out and in again to receive the new claim. remove_app_admin_privilege_for_user(uuid) reverses it. The seed grants the role to testadmin@usenextbase.com the same way.

Access-token hook

The migrations also define public.custom_access_token_hook, which would copy user_roles into the JWT on every token refresh, but the [auth.hook.custom_access_token] section in config.toml is commented out. Admin access works through make_user_app_admin without it. If you enable the hook in your Supabase project, keep public.user_roles as the source of truth.

Where the admin area lives

Administration is a separate deployment, apps/admin, running on http://localhost:3002 locally and on its own origin in production (APP_URL of the admin app). It serves /admin, /admin/users, /admin/workspaces, /admin/billing, /admin/marketing and /admin/feedback, and / redirects to /admin. Admins also get the Admin MCP connection page at /mcp/connections/admin on that origin. The web app no longer registers any /admin route; both apps share the same Supabase project and RLS rules, and every privileged request re-checks the admin claim with requireAdmin before touching serviceSupabase().

Support sign-in from /admin/users uses the service-role client to call auth.admin.generateLink({ type: "magiclink" }) for the chosen user and builds a link to the web app's /auth/confirm (WEB_APP_URL) with the hashed token and next=/app. The web app verifies the token and sets its own host-scoped session cookies; the admin session stays on the admin origin. The link can be copied or emailed through Resend when that integration is configured. See docs/admin-app.md in the repository.

What is protected, and how

  • Row-level security is the real tenant boundary. Every table has RLS enabled with explicit grants for anon, authenticated and service_role. Helper predicates include public.is_workspace_member, public.can_write_workspace_resources, public.is_workspace_admin and public.is_application_admin.
  • Workspace roles are owner, admin, member and readonly in workspace_members.workspace_member_role, plus a permissions JSONB with flags such as view_projects and manage_billing. Loaders still check membership (for example requireWorkspaceAccess in routes/workspace.tsx) to return clean 403 and 404 responses.
  • Atomic writes go through security definer SQL functions with set search_path = '', such as create_workspace_with_owner, instead of chains of Data API calls.
  • Service role usage is confined to src/.server/ and *.server.ts modules, after requireUser or requireAdmin.
  • Form actions are only accepted from the app's own origin (isCanonicalOriginRequest in src/.server/url.ts, derived from APP_URL). Each build's smoke test posts to a sign-in route from a foreign origin and expects 400.
  • Agent connections re-check the live Supabase session, confirmed email, ban state and current role or membership on every tool call through public.mcp_live_actor. See agent connections.

The pgTAP suite in apps/database/supabase/tests/ covers the allowed path, cross-tenant denial and restricted-role denial for these boundaries. Run it with pnpm test-db.

Environment variables

Every variable in apps/web/.env.example and apps/admin/.env.example, what it does and where its value comes from.

Agent connections

The User MCP and Admin MCP in the TanStack + Supabase starter: endpoints, tools, OAuth behaviour and how to extend them.

On this page

How a request is authenticatedSign-up, login and recoveryEmail confirmation and the local inboxRedirect URLsSocial providersBecoming an application adminWhere the admin area livesWhat is protected, and how