ASK KNOX
beta
LESSON 252

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.

6 min read

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:

  1. User clicked "Login with Discord" on www.jeremyknox.ai
  2. signInWithOAuth sent the user to GoTrue without a redirectTo parameter
  3. GoTrue completed the Discord OAuth flow
  4. GoTrue redirected to Site URL: https://jeremyknox.ai/auth/callback?code=...
  5. Vercel issued a 307 redirect from jeremyknox.ai to www.jeremyknox.ai
  6. 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.

  1. What is your Site URL? Does it match the exact origin where your app's auth callback lives?
  2. What is in your Redirect Allowlist? Are all your deployment environments covered?
  3. In your Supabase client initialization, what is flowType? If it is not set, the default is pkce.

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.