Switch Language
Toggle Theme

Supabase Auth in Practice: Email Verification, OAuth & Session Management

Opening the Supabase Dashboard, clicking into the Authentication section, I froze. Email verification, Magic Link, OAuth, SSR configuration… so many options. Which one should I use? How do I configure them?

If you’re facing the same confusion, don’t panic. I once spent half a day adding login functionality to a small project before realizing—each authentication method has its ideal use case. This article walks you through three core Supabase Auth approaches: email verification, OAuth integration, and that headache-inducing session management. By the end, you’ll be able to set up a complete user authentication system in about 30 minutes.

Email Verification — The Foundation of Authentication

Honestly, email verification is the simplest yet most overlooked part of Supabase Auth.

Open your Dashboard, navigate to Authentication → Providers → Email, and you’ll see a toggle called “Confirm Email.” This switch determines whether users need to verify their email before logging in. Hosted projects have this enabled by default—users receive a verification email after registration and must click the link to activate their account.

I accidentally turned this off during my first configuration. The result? Anyone could log in with any made-up email address, and spam accounts flooded the system. Later I learned this switch is mandatory in production.

The configuration code is straightforward:

// Trigger email verification on sign-up
const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'secure-password',
  options: {
    emailRedirectTo: 'https://yourapp.com/auth/callback'
  }
})

Here’s a detail worth noting: the emailRedirectTo parameter. After users click the verification link in the email, they’ll be redirected to this address. You can point it to your app’s homepage or a dedicated welcome page.

Speaking of email templates, Supabase includes several built-in templates: confirm email, password reset, and Magic Link emails. You can edit them directly in the Dashboard’s Email Templates section. If you want to use your own SMTP service like Resend or SendGrid, configure it in Auth Hooks. That’s an advanced setup though—the default templates are sufficient for getting started.

I’ve hit another pitfall: email verification can get stuck in local development. The reason is that Supabase local instances don’t send real emails by default. You can use tools like Mailcatcher to view test emails, or simply turn off Confirm Email during local development and re-enable it before going live.

OAuth Integration — One-Click Login

OAuth login is a game-changer for user experience. Users don’t need to remember passwords—they just click a GitHub or Google button to log in, and conversion rates are usually higher than email registration.

Supabase supports many OAuth providers: GitHub, Google, Facebook, Apple, Azure, Twitter, Discord… over 15 in total. I use GitHub and Google most frequently—these two have the clearest configuration processes.

Let’s start with GitHub OAuth. First, create an OAuth App on GitHub (Settings → Developer settings → OAuth Apps → New OAuth App). The key is getting the Callback URL right:

https://<your-project-ref>.supabase.co/auth/v1/callback

For local development, use this:

http://localhost:54321/auth/v1/callback

Then copy the GitHub OAuth App’s Client ID and Client Secret to your Supabase Dashboard (Authentication → Providers → GitHub). After saving, the client-side call is simple:

// GitHub OAuth login
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'https://yourapp.com/auth/callback'
  }
})

Google OAuth follows a similar process, but with one difference: Google requires separate Client IDs for Web, iOS, and Android platforms. If your app supports both web and mobile, you’ll need to configure each.

By the way, after OAuth login, Supabase provides a provider token. This token can call third-party APIs—for example, using the GitHub token to fetch a user’s repository list, or the Google token to access Google Drive. This feature is particularly useful for apps that need to integrate third-party services.

OAuth has its pitfalls too: incorrect callback URL configuration is a high-frequency error in local development. I once wrote port 3000 (my frontend port) and got an error after login. Only later did I realize the callback must point to Supabase’s port, not the frontend’s.

Session Management — JWT & PKCE Flow

This chapter might be the most confusing part. Honestly, I didn’t fully understand how JWT, refresh tokens, and PKCE fit together at first.

Supabase sessions consist of two parts: access token (short-lived JWT) and refresh token (long-lived token). The access token has a default validity of 1 hour—the official recommendation is not to go below 5 minutes due to clock skew issues. Refresh tokens are single-use, used to exchange for new access tokens.

Here’s an important detail: refresh tokens have a 10-second reuse window. What does this mean? In SSR environments, if multiple requests try to refresh tokens simultaneously, Supabase allows duplicate refresh operations within 10 seconds, preventing unexpected session termination. This design makes sense—situations where frontend and backend simultaneously operate on sessions are common.

Now about PKCE. If you’re using Next.js or other SSR frameworks, PKCE flow is mandatory. Why? Implicit flow exposes tokens directly in URLs, which is insecure in SSR environments. PKCE protects the token exchange process with a code verifier.

Configuring PKCE requires two parameters during client initialization:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
  auth: {
    detectSessionInUrl: true,
    flowType: 'pkce'
  }
})

Then you need a callback route to handle code exchange:

// Next.js App Router - app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return NextResponse.redirect(`${origin}/dashboard`)
    }
  }
  return NextResponse.redirect(`${origin}/auth/error`)
}

The auth code has only a 5-minute validity and can be exchanged only once. If you find code exchange failing during debugging, it’s likely timed out or reused.

Supabase also supports three session restriction modes: Time-boxed (forced expiration after fixed duration), Inactivity timeout (expires after prolonged user inactivity), and Single session per user (one account can only have one active session). These modes are particularly useful for applications requiring SOC 2 or HIPAA compliance.

Practical Advice & Common Issues

After all this, you might wonder: which authentication method should I choose?

Simply put: Email verification suits registration flows that need user information, like when users need to fill out complete profiles; OAuth suits scenarios prioritizing quick login, like developer tools or B2B applications; Magic Link suits passwordless scenarios, like temporary access or mobile-first applications.

Here’s a comparison of the three methods:

MethodUse CaseProsCons
Email VerificationFormal registrationComplete info, high controlUsers must remember passwords
OAuthQuick loginPasswordless, high conversionDepends on third-party stability
Magic LinkPasswordless scenariosSecure, simpleMust check email for every login

If you’re using Next.js or other SSR frameworks, here’s a configuration checklist:

  1. detectSessionInUrl: true — Let Supabase automatically extract session from URL
  2. flowType: 'pkce' — Enforce PKCE flow
  3. redirectTo configured correctly — Callback route must properly handle auth code
  4. Environment variables checked — Ensure NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are configured

A few common questions:

Q: Why does OAuth keep failing in local development?

Most likely the callback URL is wrong. Check your Supabase Dashboard provider settings—make sure you’re using localhost addresses, not production domain names.

Q: Users forced to log out after JWT expires?

Make sure your client has automatic refresh mechanism. Supabase’s onAuthStateChange listener handles token refresh automatically—no need to write manual refresh logic.

Q: Session suddenly disappears?

This issue is common in SSR environments. Check that both your server client and browser client are properly initialized, especially whether cookies are transmitted correctly.

Conclusion

Supabase Auth’s three authentication methods each serve their purpose. Email verification is foundational, suitable for formal registration flows; OAuth enhances user experience, perfect for quick-login scenarios; session management is the underlying infrastructure—understanding JWT and PKCE flow is essential for correct SSR configuration.

The pitfalls I’ve encountered are actually quite simple—wrong callback URL, forgetting to turn on confirm email, not configuring PKCE flow. Once you understand these details, your authentication system will run smoothly.

Next step recommendation: After configuring Auth, don’t forget to protect your data with Row Level Security (RLS). Supabase’s RLS is tightly integrated with Auth—each user can only access their own data. That’s the complete closed loop of an authentication system.

Configure Complete Supabase Auth Flow

From email verification to OAuth integration, to PKCE configuration for SSR environments

⏱️ Estimated time: 30 min

  1. 1

    Step1: Enable Email Verification

    Configure in Supabase Dashboard:

    • Navigate to Authentication → Providers → Email
    • Enable the Confirm Email toggle
    • Configure emailRedirectTo parameter to point to your app's callback address
    • Use Mailcatcher for testing emails in local development
  2. 2

    Step2: Configure GitHub OAuth

    Establish OAuth connection between GitHub and Supabase:

    • Create OAuth App on GitHub (Settings → Developer settings → OAuth Apps)
    • Set Callback URL: https://&lt;ref&gt;.supabase.co/auth/v1/callback
    • For local development: http://localhost:54321/auth/v1/callback
    • Copy Client ID and Client Secret to Supabase Dashboard
  3. 3

    Step3: Configure PKCE Flow

    Set up secure authentication flow for SSR environments (Next.js):

    • Set flowType: 'pkce' during client initialization
    • Enable detectSessionInUrl: true
    • Create /auth/callback route to handle code exchange
    • Auth code is valid for 5 minutes, single use only
  4. 4

    Step4: Handle Session Refresh

    Ensure session persistence:

    • Access token expires in 1 hour by default
    • Refresh token is single-use with 10-second reuse window
    • Client listens to onAuthStateChange for automatic refresh
    • In SSR environments, ensure proper cookie transmission

FAQ

Which OAuth providers does Supabase Auth support?
Supports 15+ providers, including GitHub, Google, Facebook, Apple, Azure, Twitter, Discord, GitLab, Bitbucket, LinkedIn, Twitch, Spotify, Slack, Notion, and more. GitHub and Google are most commonly used.
What's the default expiration time for JWT access tokens?
Default is 1 hour. Official recommendation is not to go below 5 minutes (considering clock skew). Can be adjusted through Dashboard. Refresh tokens are single-use, used to exchange for new access tokens.
Why is PKCE flow required for SSR environments?
Implicit flow exposes tokens in URLs, which is insecure in SSR environments. PKCE protects token exchange with a code verifier. Auth code expires in 5 minutes and can only be exchanged once.
Why does OAuth callback keep failing in local development?
Check three things: 1) Whether the provider's callback URL in Supabase Dashboard uses localhost; 2) Whether the port number is correct (Supabase local port is 54321); 3) It's not the frontend port (like 3000).
What is the refresh token reuse window?
A 10-second window that prevents session termination when multiple requests refresh simultaneously in SSR scenarios. When frontend and backend operate on session concurrently, duplicate refresh operations within 10 seconds are allowed.
How should I choose between the three authentication methods?
Email verification suits formal registration flows (requiring complete user info); OAuth suits quick-login scenarios (developer tools, B2B applications); Magic Link suits passwordless scenarios (temporary access, mobile-first).

8 min read · Published on: Apr 8, 2026 · Modified on: Apr 8, 2026

Comments

Sign in with GitHub to leave a comment

Related Posts