The boilerplate supports multiple authentication methods:

Magic Link (Email)
Google OAuth
GitHub OAuth
Account Linking
Protected Routes
User Onboarding
Login Page

Login page with magic link and OAuth providers

A single sign-in request produces two equivalent paths — a clickable link and a one-time code (default 6 digits, configurable 6–10 via OTP_LENGTH) — so users who request on one device (desktop) and open the email on another (mobile) never lose the session. The email body shows the code prominently above the button; the success screen in the original tab includes an OTP input so users can type the code back without context-switching.

  1. User enters their email

    On the login page, the user submits their email address. The login button reads "Continue with email" (i18n key auth.login.sendMagicLink) — intentionally jargon-free.

  2. API sends link and code

    The /api/auth/magic-link endpoint calls Supabase admin.generateLink({ type: 'magiclink' }). The response carries both properties.action_link AND properties.email_otp. We surface both: the link goes into the email button, the code into a prominent monospace block above it. The email is sent through the active provider (Brevo / Mailjet) using the locale-appropriate template (FR/EN).

  3. Success screen on the original tab

    The form switches to a "Check your inbox" view that shows the email address the link was sent to, an OTP input (numeric, autoComplete="one-time-code", dynamic maxLength/pattern based on OTP_LENGTH), a "Wrong email?" back-button, a spam-folder reminder, and a "Resend" button with a 30-second cooldown countdown.

  4. Path A — user clicks the link

    The link points to /[locale]/callback?token_hash=…&type=magiclink. The callback validates the OTP type against an allowlist (prevents token-type-confusion attacks), calls supabase.auth.verifyOtp, re-checks the prelaunch allowlist (belt-and-suspenders), logs the login event, and redirects to the sanitized redirectTo.

  5. Path B — user types the code

    The OTP input submits to POST /api/auth/verify-otp (apiSecurity.public() + strict rate limit). The endpoint Zod-validates { email, token, locale, redirectTo } (regex built dynamically from appConfig.auth.otpLength), calls supabase.auth.verifyOtp({ type: 'email', email, token }), mirrors the callback's prelaunch gate and login logging, then returns the resolved redirect target as JSON. The client does a full reload so middleware re-reads the freshly-issued session cookie. On failure: generic "Invalid or expired code" — never confirms whether the email is registered (no enumeration vector).

  6. Cross-tab auto-redirect

    While the success screen is open, the client polls supabase.auth.getUser() every 4 seconds (paused via document.visibilityState when the tab is backgrounded). If the user clicks the email link in a different tab, that tab's callback route sets the auth cookie — cookies are shared per origin, so the original tab's next poll sees the user and auto-redirects. No service worker, no broadcast channel, no extra dependency.

Post-event routing & B2B workspace requirement

When the sanitized redirectTo is the default /private-dashboard (typically because the user logged in without an explicit destination), the callback consults isWorkspaceManager(user.id) from lib/permissions/check.ts and swaps to /org-dashboard if the user owns or admins at least one workspace. Explicit non-default targets (e.g. /checkout coming from the pricing flow) flow through untouched. The same swap applies to the OAuth path and the OTP-code path — one helper, three call sites, in lock-step with the /org-dashboard layout gate (role_slug IN ('owner', 'admin')).

In B2B mode (appConfig.businessModel === 'b2b'), subscription billing must attach to a workspace account. The happy path auto-creates that workspace during login/checkout bootstrap through ensureWorkspaceForUser(userId, t('auth.defaultWorkspaceName')). The helper is wired at five sites: the magic-link callback, the OAuth callback, POST /api/auth/verify-otp, the /checkout Server Component, and the free-plan path through /api/billing/subscribe-free (the route delegates to subscribeToFreePlan in core/billing/free-plan.ts, which calls ensureWorkspaceForUser). The legacy manual page /[locale]/onboarding/workspace still exists as a recovery path when bootstrap fails.

The unified purchase funnel is /pricing → /login → /checkout → Stripe or subscribe-free → /checkout/success → /onboarding (if needed) → dashboard. In B2B, login and checkout call the workspace bootstrap before payment, then the dashboard target becomes /org-dashboard for workspace owners/admins. Defense-in-depth still exists: /api/billing/checkout returns 400 WORKSPACE_REQUIRED for personal-account subscription attempts, and /api/billing/subscribe-free resolves the workspace in B2B.

Marketing shell authentication state

The public marketing shell is intentionally anonymous-first. It renders static HTML, then MarketingAuthProvider fetches GET /api/auth/me once after hydration to update the navbar, account dropdown, workspace links, and command palette. This avoids calling getUser() from the marketing layout, which would make every public page dynamic.

/api/auth/me validates the current Supabase session with server-side getUser(), reads the minimal marketing auth context from core/accounts/queries.ts, and returns only sanitized display fields. The route is read-only, cache-disabled with Cache-Control: private, no-store, and uses the relaxed 100/min rate-limit tier so normal page navigation does not trip the limiter.

Authenticated visitors may briefly see the anonymous Login link before hydration finishes. Clicking it is still safe: the existing auth-route redirect sends already-authenticated users to their dashboard.

Configuring the code length (OTP_LENGTH)

The number of digits in the code is configurable via the OTP_LENGTH environment variable. Supabase Auth itself supports 6 to 10 digits (set in Authentication → Email OTP length in the Supabase dashboard); the env var here tells our UI/API how many digits to expect.

# .env.local — must match Supabase Auth → Email OTP length
OTP_LENGTH=6
  • Server-only — no NEXT_PUBLIC_ prefix. The login page (Server Component) reads appConfig.auth.otpLength and passes the resolved number to the client form as a prop. The client never reads process.env directly.
  • Validated at startuplib/env.ts uses z.coerce.number().int().min(6).max(10).default(6). Bad values fail boot, not individual requests.
  • Single source of truthconfig/app.ts exposes appConfig.auth.otpLength; nothing reads process.env.OTP_LENGTH outside lib/env.ts / config/app.ts. The regex in /api/auth/verify-otp, the maxLength + pattern on the OTP input, and the dynamic placeholder all derive from this value.
  • Operator caveat — a mismatch between OTP_LENGTH and the Supabase setting makes every verify fail. To change: update both, then redeploy.

OAuth Providers

The boilerplate ships with a config-driven OAuth catalog at lib/auth/oauth-providers.tsx. The login page reads appConfig.auth.oauthProviders (derived from NEXT_PUBLIC_OAUTH_PROVIDERS) and renders one branded button per enabled provider — no hardcoded buttons in the form.

Dynamic Provider Configuration

Set the providers you want surfaced on the login page via a comma-separated env var:

# Comma-separated, case-insensitive. Order matters (top-to-bottom render order).
NEXT_PUBLIC_OAUTH_PROVIDERS=google,github,apple
Two-step contract (no auto-discovery)

Supabase has no public runtime API for listing which providers are enabled in a project, so the button list cannot auto-sync with the dashboard. To add a provider:

  1. Enable it in your Supabase Auth dashboard (add Client ID + Secret).
  2. Append its id to NEXT_PUBLIC_OAUTH_PROVIDERS and redeploy.

Unknown ids in the env var are silently dropped at render time (getOAuthProviderEntries() validates each id against the catalog). The NEXT_PUBLIC_ prefix is intentional: the list is purely a UI hint, so leaking it to the browser is harmless — the actual provider Client IDs and Secrets stay in Supabase.

Supported Provider Ids

The catalog covers every id accepted by the installed @supabase/auth-js Provider type union (19 total). Use these exact strings — anything else is dropped:

Id (env value) Display label Notes
appleAppleApple Sign In
azureMicrosoftAzure AD / Entra ID
bitbucketBitbucketAtlassian Bitbucket
discordDiscord
facebookFacebookMeta / Facebook
figmaFigma
githubGitHub
gitlabGitLab
googleGoogleDefault in .env.example
kakaoKakaoKorean chat platform
keycloakKeycloakSelf-hosted / generic OIDC
linkedinLinkedIn (legacy)Deprecated by LinkedIn — use linkedin_oidc
linkedin_oidcLinkedInCurrent LinkedIn OIDC integration
notionNotion
slackSlack (legacy)Deprecated — use slack_oidc
slack_oidcSlackCurrent Slack OIDC integration
spotifySpotify
twitchTwitch
twitterX (Twitter)OAuth 1.0a

Leaving NEXT_PUBLIC_OAUTH_PROVIDERS= empty renders the magic-link form only — no buttons and no divider.

Extending the Catalog

To add a provider that isn't in the SDK's Provider union yet (e.g. a future addition or a custom branded icon), edit lib/auth/oauth-providers.tsx: define an SVG icon constant and add a row to OAUTH_PROVIDER_CATALOG. TypeScript will enforce that the id matches an SDK-accepted provider string — signInWithOAuth({ provider }) won't accept anything else.

Google OAuth Configuration

Follow these steps to enable Google Sign-In for your application:

Step 1: Create a Google Cloud Project
  1. Go to Google Cloud Console
  2. Click "Select a project""New Project"
  3. Enter a project name (e.g., "My SaaS App") and click "Create"
  4. Wait for the project to be created, then select it
Step 2: Configure OAuth Consent Screen
  1. In the left sidebar, navigate to "APIs & Services""OAuth consent screen"
  2. Choose "External" (unless you have a Google Workspace organization)
  3. Fill in the required fields:
    • App name: Your application name
    • User support email: Your email address
    • App logo: Upload your logo (optional)
    • App domain: Your production domain (e.g., myapp.com)
    • Developer contact: Your email address
  4. Click "Save and Continue"
  5. On the "Scopes" page, click "Add or Remove Scopes"
  6. Select the following scopes:
    • .../auth/userinfo.email
    • .../auth/userinfo.profile
    • openid
  7. Click "Save and Continue"
  8. Add test users if needed (for development), then click "Save and Continue"
Step 3: Create OAuth Credentials
  1. Go to "APIs & Services""Credentials"
  2. Click "+ Create Credentials""OAuth client ID"
  3. Select "Web application" as the application type
  4. Enter a name (e.g., "Supabase Auth")
  5. Under "Authorized JavaScript origins", add:
    • https://<your-project-ref>.supabase.co
  6. Under "Authorized redirect URIs", add:
    • https://<your-project-ref>.supabase.co/auth/v1/callback
  7. Click "Create"
  8. Copy the Client ID and Client Secret - you'll need these for Supabase
Important: Replace <your-project-ref> with your actual Supabase project reference (found in your Supabase dashboard URL).
Step 4: Configure Supabase
  1. Go to your Supabase Dashboard
  2. Select your project
  3. Navigate to "Authentication""Providers"
  4. Find "Google" in the list and click to expand
  5. Toggle "Enable Sign in with Google" to ON
  6. Enter the Client ID and Client Secret from Google
  7. Click "Save"
Step 5: Implement in Your App

No code changes needed — the dynamic OAuth catalog handles rendering. After enabling Google in the Supabase dashboard, ensure google is present in NEXT_PUBLIC_OAUTH_PROVIDERS (it is by default in .env.example) and the branded Google button appears on the login page. On the server side, the callback route at app/[locale]/(auth)/callback/route.ts exchanges the authorization code for a session and redirects the user to the dashboard.

Step 6: Handle the Callback

The callback route at app/[locale]/(auth)/callback/route.ts receives the authorization code from the OAuth provider, exchanges it for a Supabase session using exchangeCodeForSession(), and redirects the user to their dashboard. If the exchange fails, the user is redirected back to the login page with an error message.

Production Checklist
Before going live:
  • Publish your OAuth consent screen (move from "Testing" to "Production")
  • Add your production domain to authorized origins
  • Verify your domain ownership in Google Search Console
  • Submit for Google verification if requesting sensitive scopes

GitHub OAuth Configuration

GitHub OAuth is another popular option for developer-focused applications:

Step 1: Create GitHub OAuth App
  1. Go to GitHub Developer Settings
  2. Click "New OAuth App"
  3. Fill in the required fields:
    • Application name: Your app name
    • Homepage URL: https://yourapp.com
    • Authorization callback URL: https://<your-project-ref>.supabase.co/auth/v1/callback
  4. Click "Register application"
  5. Click "Generate a new client secret"
  6. Copy both the Client ID and Client Secret
Step 2: Configure in Supabase
  1. Go to "Authentication""Providers" in Supabase
  2. Find "GitHub" and toggle it ON
  3. Paste your Client ID and Client Secret
  4. Click "Save"
Implement GitHub Sign-In

Add github to NEXT_PUBLIC_OAUTH_PROVIDERS (e.g. google,github) and redeploy — the branded GitHub button is rendered automatically by the dynamic catalog. No frontend code changes required.

Other OAuth Providers

Supabase supports many other providers. The configuration process is similar: enable in the dashboard, then append the provider id to NEXT_PUBLIC_OAUTH_PROVIDERS. See the supported ids table above for exact env values.

Provider Console URL Callback URL Format
Apple Apple Developer https://<ref>.supabase.co/auth/v1/callback
Discord Discord Developers https://<ref>.supabase.co/auth/v1/callback
Microsoft Azure Portal https://<ref>.supabase.co/auth/v1/callback
Twitter/X Twitter Developer https://<ref>.supabase.co/auth/v1/callback
LinkedIn LinkedIn Developers https://<ref>.supabase.co/auth/v1/callback
Facebook Meta Developers https://<ref>.supabase.co/auth/v1/callback

OAuth Troubleshooting

Error Cause Solution
redirect_uri_mismatch Callback URL doesn't match Ensure the callback URL in your provider matches exactly: https://<ref>.supabase.co/auth/v1/callback
access_denied User denied consent User clicked "Deny" - handle gracefully in your UI
invalid_client Wrong credentials Double-check your Client ID and Secret in Supabase
popup_closed_by_user Popup was closed User closed the popup - show a retry option
No email returned Missing email scope Add email scope to your OAuth configuration
CORS errors Wrong origin configured Add your domain to authorized JavaScript origins
Tip: When testing locally, Supabase automatically handles localhost redirects. For production, make sure all URLs use HTTPS.

Advanced Auth Features

The authentication system includes several advanced features:

Feature Description
Direct Magic Link Verification Custom callback handler bypasses Supabase's redirect flow for cleaner URLs. Verifies token_hash directly.
OAuth Account Linking Automatically detects existing accounts with same email, merges OAuth data (avatar, name), cleans up duplicates.
Dashboard Access Control Private dashboards (/private-dashboard, /org-dashboard, etc.) require an active plan or license. /checkout is intentionally not gated — the checkout-before-onboarding workflow lets users pay before completing their profile. The single onboarding gate lives in app/[locale]/(auth)/checkout/success/page.tsx.
Automatic Account Creation Personal account auto-created on signup via the handle_new_user_account trigger on auth.users INSERT, ensuring users always have an account for billing. In B2B mode, a workspace account is auto-bootstrapped on the same signup flow via ensureWorkspaceForUser.

Email Templates

All transactional emails support multi-language (FR/EN) through the active email provider:

  • Magic Link - Custom templates with app branding
  • Workspace Invitation - Includes inviter name, workspace name, role
  • Contact Form - Admin notification + user confirmation
  • Organization Deletion - Member notification before deletion

Email content uses getEmailTranslation() helper with dynamic app name from environment variable.