Supabase Auth — GoTrue Under the Hood
Supabase doesn't do auth — GoTrue does. Understanding this open-source OAuth middleware layer explains every mysterious redirect, every configuration setting, and why your callback URL matters so much.
Supabase does not implement authentication.
GoTrue does. Supabase wraps it.
This distinction matters more than it seems. When you configure "Supabase Auth," you are actually configuring GoTrue — an open-source Go service developed by Netlify, adopted by Supabase, and deployed as one of the microservices in your Supabase project. Every authentication endpoint, every redirect, every token issued: that is GoTrue.
Understanding GoTrue's architecture explains behavior that otherwise looks like Supabase magic.
GoTrue as OAuth Middleware
GoTrue sits between your application and every identity provider (Discord, Google, GitHub, Apple, email/password). It normalizes all of them into a single session format.
Your App
↓ signInWithOAuth({ provider: 'discord' })
GoTrue /authorize endpoint
↓ redirects to
Discord OAuth2 /authorize
↓ user approves
Discord /callback → GoTrue (with Discord's code)
↓ GoTrue exchanges Discord code for Discord token (server-to-server)
↓ GoTrue creates its own session
↓ GoTrue issues its own authorization code
GoTrue redirects to your app
↓ your app receives GoTrue's code (NOT Discord's)
Your app /auth/callback
↓ exchangeCodeForSession(code)
↓ GoTrue exchanges its code for a JWT session token
User is authenticated
Your application never touches Discord's OAuth flow directly. You receive a GoTrue code, exchange it for a GoTrue session, and GoTrue handles the Discord credential lifecycle internally. When the GoTrue session expires, GoTrue refreshes the Discord token automatically.
The Two Critical Configuration Settings
Site URL
The Site URL is GoTrue's fallback. When an OAuth flow completes but there is no redirectTo parameter (or the redirectTo is not in the allowlist), GoTrue sends the user to the Site URL.
# Supabase Dashboard → Authentication → URL Configuration
Site URL: https://jeremyknox.ai
# This causes a problem when your app runs at:
# https://www.jeremyknox.ai (note: www subdomain)
# or
# https://academy.jeremyknox.ai (note: different subdomain)
If your app lives at www.jeremyknox.ai but Site URL is jeremyknox.ai, every auth callback goes to the apex domain. If Vercel redirects jeremyknox.ai to www.jeremyknox.ai, your PKCE verifier may be lost in the redirect. If there is no redirect, the callback lands on a page that is not your app.
The Site URL must match exactly where your app expects to receive the callback.
Redirect Allowlist
GoTrue validates every redirectTo parameter against an explicit allowlist. You register patterns like:
https://www.jeremyknox.ai/**
https://academy.jeremyknox.ai/**
http://localhost:3000/**
If your app calls signInWithOAuth({ provider: 'discord', options: { redirectTo: 'https://academy.jeremyknox.ai/auth/callback' } }) but https://academy.jeremyknox.ai/** is not in the allowlist, GoTrue falls back to the Site URL instead of using your redirect. The user ends up on the wrong domain.
The Client Initialization Settings
The Supabase browser client has auth options that interact directly with GoTrue's behavior:
const supabase = createBrowserClient(url, anonKey, {
auth: {
flowType: 'pkce', // Must match GoTrue's configured flow
detectSessionInUrl: true, // Auto-process ?code= callbacks (default: true)
autoRefreshToken: true, // Auto-refresh before expiry (default: true)
persistSession: true, // Store session in storage (default: true)
storageKey: 'sb-{project-ref}-auth-token' // DO NOT override this without understanding chunking
}
})
detectSessionInUrl is the silent workhorse. When enabled, every page load scans the URL for ?code= or #access_token=. When found, the client calls exchangeCodeForSession and processes the OAuth callback automatically. This means your /auth/callback page does not need to do anything special — the client handles it.
The danger: if detectSessionInUrl: true AND you also manually call exchangeCodeForSession in your callback route handler, both will race to exchange the same code. The code can only be exchanged once. One will succeed, one will get an error, and depending on timing, your session may or may not be established.
The storageKey: Why Not to Override It
Supabase stores the session under a specific key: sb-{project-ref}-auth-token. The @supabase/ssr package may chunk this across multiple cookies if the token exceeds 4KB (common when Discord returns large user metadata).
The chunking keys follow a pattern: sb-{project-ref}-auth-token.0, sb-{project-ref}-auth-token.1, etc.
If you override storageKey to a custom value, the chunking logic still runs, but the base key changes. The client reads back from custom-key.0, custom-key.1 — which works. But if any other part of your stack (middleware, server components) initializes a client without the custom key, it will not find the session.
Rule: Do not override storageKey unless you are overriding it everywhere consistently.
Case Study: Site URL Mismatch — The Wrong Domain Redirect
In the April 2026 debugging session, the Supabase project had Site URL set to https://jeremyknox.ai. The production app was deployed at https://www.jeremyknox.ai. Vercel was configured to redirect jeremyknox.ai to www.jeremyknox.ai.
The auth flow:
- User clicked "Login with Discord" on
www.jeremyknox.ai signInWithOAuthsent the user to GoTrue without aredirectToparameter- GoTrue completed the Discord OAuth flow
- GoTrue redirected to Site URL:
https://jeremyknox.ai/auth/callback?code=... - Vercel issued a 307 redirect from
jeremyknox.aitowww.jeremyknox.ai - The PKCE code verifier, stored in sessionStorage on
www.jeremyknox.ai, was gone — the redirect chain had navigated away and back
The user arrived at www.jeremyknox.ai/auth/callback?code=... with a valid code but no verifier in storage. The code exchange failed silently. The user was not logged in.
The fix: change Site URL in the Supabase dashboard from https://jeremyknox.ai to https://www.jeremyknox.ai. One configuration change. The PKCE verifier survived because the flow stayed on the same origin throughout.
Lesson 4 Drill
Open your Supabase project dashboard. Navigate to Authentication → URL Configuration.
- What is your Site URL? Does it match the exact origin where your app's auth callback lives?
- What is in your Redirect Allowlist? Are all your deployment environments covered?
- In your Supabase client initialization, what is
flowType? If it is not set, the default ispkce.
Then check: does your /auth/callback route manually call exchangeCodeForSession? Is detectSessionInUrl also enabled? If both are true, you have a race condition waiting to surface.