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·Authentication Architecture

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 and exchange it for a GoTrue session. One caveat: GoTrue refreshes only its OWN session token. The Discord tokens are surfaced to your app exactly once, at sign-in, as provider_token and provider_refresh_token on the session — Supabase does not store or refresh them for you. If you need ongoing Discord API access, persisting and refreshing those provider tokens is your application's responsibility.

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. The callback's first landing was the apex origin jeremyknox.ai — not www.jeremyknox.ai, where the flow had started and where the Supabase client had persisted the PKCE verifier (browser storage, including the verifier, is per-origin)
  6. The exchange ran against the wrong origin's storage. Note what did NOT happen: navigation does not erase sessionStorage — values written on www survive the round-trip to Discord and back in the same tab. The verifier was not deleted; it was unreachable from the origin where the callback executed.

The callback carried a valid code, but no verifier was reachable at the point of exchange. 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 was reachable because the flow stayed on the same origin throughout.

Lesson 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.