Back to Blog

Supabase Auth + Next.js: Complete Authentication Guide (2026)

DesignRevision Editorial DesignRevision Editorial · SaaS, frontend & developer tooling
Updated March 17, 2025 21 min read
Human Written
Share:

Authentication is the foundation of every Next.js application that handles user data. Set it up wrong and you expose user accounts, leak sessions, or spend weeks debugging cookie issues across server and client boundaries. Set it up right and your users log in, stay logged in, and their data stays protected without you thinking about it again.

Supabase Auth has become the go-to authentication solution for Next.js developers who want a full backend stack without stitching together five different services. It bundles email/password login, social authentication with Google, GitHub, and Apple, magic links, phone auth, and deep PostgreSQL integration through Row Level Security into a single platform.

This supabase auth nextjs guide walks through every authentication method, from initial setup to production deployment. You will configure email/password sign-up, add Google and GitHub OAuth, implement passwordless magic links, protect routes with Next.js 15 middleware, and handle server-side auth in React Server Components and Server Actions. Every code example targets Next.js 15 App Router with the latest @supabase/ssr package.

Key Takeaways

If you remember nothing else:

  • Supabase Auth is free for up to 50,000 monthly active users. Pro plan starts at $25/month for 100,000 MAUs
  • Use @supabase/ssr (not the legacy auth-helpers) for all Next.js 15 projects. It handles cookies, PKCE, and session refresh automatically
  • Middleware-level route protection is the recommended pattern. It catches unauthenticated requests before any server component renders
  • Social auth (Google, GitHub, Apple) uses the PKCE flow with a callback route handler that exchanges authorization codes for sessions
  • Magic links require zero password management and work through signInWithOtp with an email redirect
  • Supabase Auth integrates directly with Row Level Security for database-level authorization that no API bypass can circumvent
  • For a ready-made Next.js + Supabase Auth stack, RevKit ships with authentication, billing, and admin tooling pre-configured

Table of Contents

  1. Project Setup
  2. Creating Supabase Clients for Next.js 15
  3. Email and Password Authentication
  4. Social Authentication: Google, GitHub, and Apple
  5. Magic Link Authentication
  6. Middleware Route Protection
  7. Server-Side Auth in React Server Components
  8. Protecting API Routes
  9. Auth Callback Route Handler
  10. Session Management and Refresh
  11. Supabase Auth vs Auth.js: When to Use Which
  12. Production Checklist
  13. Conclusion

Project Setup

Start by installing the required packages. The @supabase/ssr package replaces the older @supabase/auth-helpers-nextjs and is the only supported way to use Supabase Auth with Next.js 15 App Router.

npm install @supabase/supabase-js @supabase/ssr

Add your Supabase credentials to .env.local:

NEXT_PUBLIC_SUPABASE_URL=https://your-project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

You can find both values in the Supabase Dashboard under Project Settings > API. The anon key is safe to expose on the client because Row Level Security policies control data access at the database level.

Important: Never expose your service_role key on the client side. The service role key bypasses all RLS policies and should only be used in server-side admin operations.

Creating Supabase Clients for Next.js 15

Next.js 15 has three execution contexts: browser (client components), server (server components, server actions), and middleware. Each requires a different Supabase client configuration because of how cookies are accessed in each context.

Browser Client

Create lib/supabase/client.ts for use in client components:

import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

Server Client

Create lib/supabase/server.ts for server components and server actions:

import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method is called from a Server Component
            // where cookies cannot be set. This can be safely ignored
            // if middleware refreshes user sessions.
          }
        },
      },
    }
  )
}

In Next.js 15, the cookies() function is asynchronous. The await cookies() call is required and represents a breaking change from Next.js 14.

Middleware Client

Create lib/supabase/middleware.ts for the middleware context:

import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({
            request,
          })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const {
    data: { user },
  } = await supabase.auth.getUser()

  if (
    !user &&
    !request.nextUrl.pathname.startsWith('/login') &&
    !request.nextUrl.pathname.startsWith('/auth') &&
    !request.nextUrl.pathname.startsWith('/signup')
  ) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

This three-client pattern is the standard architecture for supabase auth nextjs applications. Each client handles cookie access correctly for its execution context, and the middleware client is responsible for refreshing expired sessions automatically.

Email and Password Authentication

Email/password is the most common supabase authentication method. Create server actions for sign-up and sign-in that run on the server and handle redirects.

Sign-Up Server Action

// app/signup/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export async function signup(formData: FormData) {
  const supabase = await createClient()

  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }

  const { error } = await supabase.auth.signUp(data)

  if (error) {
    redirect('/error')
  }

  revalidatePath('/', 'layout')
  redirect('/verify-email')
}

Sign-In Server Action

// app/login/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export async function login(formData: FormData) {
  const supabase = await createClient()

  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  }

  const { error } = await supabase.auth.signInWithPassword(data)

  if (error) {
    redirect('/error')
  }

  revalidatePath('/', 'layout')
  redirect('/dashboard')
}

Sign-Up Form Component

// app/signup/page.tsx
import { signup } from './actions'

export default function SignupPage() {
  return (
    <form>
      <label htmlFor="email">Email:</label>
      <input id="email" name="email" type="email" required />

      <label htmlFor="password">Password:</label>
      <input id="password" name="password" type="password" required />

      <button formAction={signup}>Sign up</button>
    </form>
  )
}

Server Actions in Next.js 15 handle form submissions without client-side JavaScript. The form posts directly to the server action, which creates the Supabase session and redirects. This pattern avoids exposing auth logic on the client.

Sign-Out

// app/auth/signout/actions.ts
'use server'

import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export async function signout() {
  const supabase = await createClient()
  await supabase.auth.signOut()
  redirect('/login')
}

Social Authentication: Google, GitHub, and Apple

Social auth reduces friction by letting users log in with accounts they already have. Supabase Auth supports over 20 OAuth providers natively, including Google, GitHub, Apple, Discord, Twitter, and Microsoft.

Provider Configuration

Before writing code, configure each provider in the Supabase Dashboard:

  1. Go to Authentication > Providers
  2. Enable the provider (Google, GitHub, or Apple)
  3. Add the Client ID and Client Secret from the provider's developer console
  4. Set the redirect URL to https://your-project-ref.supabase.co/auth/v1/callback

For Google: Create OAuth credentials in the Google Cloud Console. Add http://localhost:3000 to authorized origins for development.

For GitHub: Create an OAuth App in GitHub Developer Settings. Set the callback URL to your Supabase project callback.

For Apple: Configure Sign in with Apple in the Apple Developer Portal. Apple requires a paid developer account and a verified domain.

Social Login Component

Social auth requires a browser redirect, so it runs in a client component:

// components/social-login.tsx
'use client'

import { createClient } from '@/lib/supabase/client'

export default function SocialLogin() {
  const supabase = createClient()

  const handleSocialLogin = async (
    provider: 'google' | 'github' | 'apple'
  ) => {
    const { error } = await supabase.auth.signInWithOAuth({
      provider,
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
      },
    })

    if (error) {
      console.error('OAuth error:', error.message)
    }
  }

  return (
    <div className="flex flex-col gap-3">
      <button onClick={() => handleSocialLogin('google')}>
        Continue with Google
      </button>
      <button onClick={() => handleSocialLogin('github')}>
        Continue with GitHub
      </button>
      <button onClick={() => handleSocialLogin('apple')}>
        Continue with Apple
      </button>
    </div>
  )
}

The redirectTo parameter tells Supabase where to send the user after they authenticate with the OAuth provider. This URL must match a callback route handler in your Next.js application (covered in the Auth Callback Route Handler section).

Requesting Additional Scopes

Some applications need access to user profile data, email addresses, or repository information beyond basic authentication. Pass scopes through the queryParams option:

const { error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: `${window.location.origin}/auth/callback`,
    queryParams: {
      access_type: 'offline',
      prompt: 'consent',
    },
    scopes: 'repo read:user',
  },
})

Magic Link Authentication

Magic links let users log in by clicking a link sent to their email. No password creation, no password reset flow, no credentials to remember. This is the fastest authentication method to implement and the easiest for users to understand.

Magic Link Server Action

// app/login/magic-link-action.ts
'use server'

import { createClient } from '@/lib/supabase/server'

export async function sendMagicLink(formData: FormData) {
  const supabase = await createClient()

  const email = formData.get('email') as string

  const { error } = await supabase.auth.signInWithOtp({
    email,
    options: {
      emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
    },
  })

  if (error) {
    return { error: error.message }
  }

  return { success: true }
}

Magic Link Form

// app/login/magic-link.tsx
'use client'

import { useState } from 'react'
import { sendMagicLink } from './magic-link-action'

export default function MagicLinkForm() {
  const [sent, setSent] = useState(false)

  async function handleSubmit(formData: FormData) {
    const result = await sendMagicLink(formData)
    if (result.success) {
      setSent(true)
    }
  }

  if (sent) {
    return <p>Check your email for the login link.</p>
  }

  return (
    <form action={handleSubmit}>
      <label htmlFor="email">Email:</label>
      <input id="email" name="email" type="email" required />
      <button type="submit">Send magic link</button>
    </form>
  )
}

When the user clicks the magic link in their email, Supabase redirects them to the emailRedirectTo URL with an authorization code. The callback route handler exchanges that code for a session.

Magic links expire after 24 hours by default. You can customize the expiration and email template in the Supabase Dashboard under Authentication > Email Templates.

Middleware Route Protection

Middleware is the single most important piece of your supabase auth nextjs setup. It runs on every request before any page renders, refreshes expired sessions automatically, and redirects unauthenticated users to the login page.

The Middleware File

Create middleware.ts at the root of your project:

// middleware.ts
import { updateSession } from '@/lib/supabase/middleware'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon)
     * - Public files (svg, png, jpg, etc.)
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

The updateSession function (defined earlier in the middleware client section) does three things on every request:

  1. Reads the Supabase auth cookies from the incoming request
  2. Refreshes the session if the access token has expired (using the refresh token)
  3. Writes the updated cookies to the response so the browser receives fresh tokens

Without middleware, expired sessions cause silent auth failures. Users appear logged in on the client but get 401 errors on server requests. This is the most common supabase authentication bug in Next.js applications.

Protecting Specific Route Groups

For more granular control, modify the updateSession function to protect only certain paths:

// Protect only /dashboard and /account routes
const protectedPaths = ['/dashboard', '/account', '/settings']
const isProtectedRoute = protectedPaths.some((path) =>
  request.nextUrl.pathname.startsWith(path)
)

if (!user && isProtectedRoute) {
  const url = request.nextUrl.clone()
  url.pathname = '/login'
  return NextResponse.redirect(url)
}

This pattern keeps marketing pages, blog posts, and landing pages publicly accessible while requiring authentication for application routes.

Server-Side Auth in React Server Components

Server components in Next.js 15 run on the server and can access Supabase Auth directly without client-side JavaScript. This is the most secure way to check authentication status because the check happens before any HTML is sent to the browser.

Protected Page Component

// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export default async function DashboardPage() {
  const supabase = await createClient()

  const { data: { user }, error } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {user.email}</p>
    </div>
  )
}

Fetching User Data with RLS

When you combine supabase auth nextjs with Row Level Security, your database queries automatically filter data based on the authenticated user. No manual WHERE user_id = ? clauses needed.

// app/dashboard/projects/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'

export default async function ProjectsPage() {
  const supabase = await createClient()

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  // RLS policies automatically filter to only
  // the authenticated user's projects
  const { data: projects } = await supabase
    .from('projects')
    .select('*')
    .order('created_at', { ascending: false })

  return (
    <div>
      <h1>Your Projects</h1>
      {projects?.map((project) => (
        <div key={project.id}>{project.name}</div>
      ))}
    </div>
  )
}

This pattern is what makes Supabase Auth fundamentally different from standalone auth providers. The authentication layer connects directly to the database authorization layer. If you set up an RLS policy like auth.uid() = user_id, every query through the Supabase client automatically respects it. Learn more in our complete Supabase RLS guide.

Auth in Server Actions

Server actions can create, update, and delete data with full auth context:

// app/dashboard/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { createClient } from '@/lib/supabase/server'

export async function createProject(formData: FormData) {
  const supabase = await createClient()

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    throw new Error('Not authenticated')
  }

  const { error } = await supabase.from('projects').insert({
    name: formData.get('name') as string,
    user_id: user.id,
  })

  if (error) {
    throw new Error(error.message)
  }

  revalidatePath('/dashboard/projects')
}

Protecting API Routes

API route handlers in Next.js 15 run on the server and need their own auth validation. Never assume that a request reaching an API route is authenticated just because the middleware allows it through.

// app/api/projects/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function GET() {
  const supabase = await createClient()

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  const { data: projects, error } = await supabase
    .from('projects')
    .select('*')

  if (error) {
    return NextResponse.json(
      { error: error.message },
      { status: 500 }
    )
  }

  return NextResponse.json(projects)
}

export async function POST(request: Request) {
  const supabase = await createClient()

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  const body = await request.json()

  const { data, error } = await supabase
    .from('projects')
    .insert({ ...body, user_id: user.id })
    .select()
    .single()

  if (error) {
    return NextResponse.json(
      { error: error.message },
      { status: 400 }
    )
  }

  return NextResponse.json(data, { status: 201 })
}

Always call getUser() instead of getSession() in API routes. The getUser() method validates the JWT against the Supabase Auth server, while getSession() only decodes the local token without verification. For security-sensitive operations, getUser() is the correct choice. This is one of the most common supabase authentication mistakes developers make.

Auth Callback Route Handler

Every OAuth flow and magic link authentication requires a callback route that exchanges an authorization code for a session. This is the bridge between the external provider and your Next.js application.

// app/auth/callback/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'

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

  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)

    if (!error) {
      const forwardedHost = request.headers.get('x-forwarded-host')
      const isLocalEnv = process.env.NODE_ENV === 'development'

      if (isLocalEnv) {
        return NextResponse.redirect(`${origin}${next}`)
      } else if (forwardedHost) {
        return NextResponse.redirect(`https://${forwardedHost}${next}`)
      } else {
        return NextResponse.redirect(`${origin}${next}`)
      }
    }
  }

  // If code exchange fails, redirect to error page
  return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}

This callback handler works for all authentication methods that use the PKCE flow:

  • Social auth (Google, GitHub, Apple) redirects here after the user approves access
  • Magic links redirect here when the user clicks the email link
  • Email confirmation links can also route through this handler

The PKCE (Proof Key for Code Exchange) flow is more secure than the implicit flow because the authorization code can only be exchanged once and requires the original code verifier that initiated the request.

Session Management and Refresh

Supabase Auth uses a dual-token system for session management:

Token Lifetime Storage Purpose
Access Token 1 hour (default) HTTP-only cookie Authorizes API requests and RLS queries
Refresh Token 7 days (default) HTTP-only cookie Obtains new access tokens silently

The @supabase/ssr package handles token refresh automatically. Here is how the flow works:

  1. User logs in. Supabase issues an access token and refresh token, stored as HTTP-only cookies
  2. User makes requests. The access token authorizes each request
  3. Access token expires (after 1 hour). The next request detects the expired token
  4. Middleware refreshes automatically. The getUser() call in middleware uses the refresh token to obtain a new access token
  5. Updated cookies are set on the response. The browser receives fresh tokens transparently

Client-Side Session Listening

For client components that need to react to auth state changes (like showing/hiding a nav bar), use the onAuthStateChange listener:

'use client'

import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import type { User } from '@supabase/supabase-js'

export default function AuthStatus() {
  const [user, setUser] = useState<User | null>(null)
  const supabase = createClient()

  useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setUser(session?.user ?? null)
      }
    )

    return () => subscription.unsubscribe()
  }, [supabase])

  return user ? <p>Logged in as {user.email}</p> : <p>Not logged in</p>
}

Supabase Auth vs Auth.js: When to Use Which

If you are choosing between Supabase Auth and Auth.js (formerly NextAuth.js) for your Next.js project, the decision comes down to your backend stack and how much control you want.

Feature Supabase Auth Auth.js v5
Pricing Free up to 50K MAUs Free (open-source)
Database PostgreSQL included Bring your own (adapters)
Social Providers 20+ built-in Plugin-based (community)
Magic Links Built-in Requires email adapter
Row Level Security Native integration Not applicable
Pre-built UI None (DIY forms) None (DIY forms)
Session Storage JWT in HTTP-only cookies JWT or database sessions
Self-hosting Yes (open-source) Yes (open-source)
Edge Runtime Full support Partial support
MFA/2FA Built-in (TOTP) Third-party required
SSO (SAML) Pro plan and above Custom implementation
Next.js 15 Support Full (via @supabase/ssr) Full (v5)

Choose Supabase Auth When:

  • You already use Supabase for your database
  • You want authentication and authorization (RLS) in one platform
  • You need 50,000 free MAUs (5x more than Clerk's free tier)
  • You want built-in magic links and phone auth without extra configuration
  • Your team wants to self-host the entire auth stack

Choose Auth.js When:

  • You use a non-PostgreSQL database (MongoDB, MySQL, DynamoDB)
  • You need complete control over every auth flow and callback
  • You want database-backed sessions with instant revocation
  • You are building a project where vendor independence is critical
  • You prefer configuring auth through code rather than a dashboard

For a deeper comparison of all major auth providers including Clerk and Firebase Auth, see our complete auth provider comparison. If you are building a SaaS and want broader implementation patterns, our SaaS authentication guide covers multi-tenant auth, RBAC, and security best practices.

What About Clerk?

Clerk offers the fastest integration for Next.js with pre-built UI components and multi-tenancy. However, it costs significantly more at scale. At 50,000 MAUs, Clerk costs approximately $800/month compared to $25/month on Supabase Auth. If you want pre-built sign-in forms and do not need database-level auth integration, Clerk is excellent. If you want cost efficiency and RLS integration, Supabase Auth wins.

Production Checklist

Before deploying your supabase auth nextjs application to production, verify every item on this list:

Authentication Configuration

  • Enable email confirmation in Supabase Dashboard (Authentication > Settings)
  • Configure custom SMTP for transactional emails (the default Supabase email has rate limits)
  • Set up a custom auth domain for branded emails
  • Configure password strength requirements (minimum 8 characters recommended)
  • Enable rate limiting on auth endpoints to prevent brute force attacks

Security

  • Verify all API routes call getUser() (not getSession()) for auth validation
  • Confirm service_role key is never exposed on the client
  • Enable Row Level Security on all tables with user data
  • Add RLS policies that use auth.uid() to scope data access
  • Set secure cookie options (HttpOnly, Secure, SameSite=Lax)
  • Review the SaaS security checklist for additional hardening

Environment and Deployment

  • Set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in your hosting provider
  • Configure redirect URLs for production domain in Supabase Dashboard
  • Add your production domain to OAuth provider allowed origins (Google, GitHub, Apple)
  • Test the full auth flow on the production domain before launch
  • Set up monitoring for auth errors in Supabase Logs Explorer

Session Management

  • Verify middleware is running on all protected routes (check the matcher pattern)
  • Test that expired sessions refresh silently without redirecting to login
  • Confirm sign-out clears all auth cookies and redirects correctly
  • Test concurrent sessions across multiple tabs and devices

For a comprehensive security review beyond authentication, our SaaS security checklist covers 50+ must-haves including infrastructure, data protection, and compliance requirements.

Skip the Setup: Use RevKit

If configuring Supabase Auth, middleware, route protection, and session management from scratch feels like a lot of wiring, it is. That is exactly why RevKit exists.

RevKit is a Next.js 15 SaaS boilerplate that ships with Supabase Auth fully configured. Email/password, social login, magic links, middleware protection, and server-side auth are all pre-built and tested. It also includes Stripe billing, admin tooling, and a shadcn/ui component library.

Instead of spending days wiring up the authentication patterns from this guide, you clone RevKit and start building your product features on day one. See how it compares to other boilerplates in our ShipFast vs Supastarter vs RevKit comparison.

Conclusion

Supabase Auth with Next.js 15 gives you a production-grade authentication system without the complexity of stitching together multiple services. The @supabase/ssr package handles the hard parts: cookie management across server and client boundaries, automatic session refresh in middleware, and secure PKCE flows for OAuth and magic links.

The key patterns to remember:

  1. Three clients for three contexts: browser, server, and middleware
  2. Middleware refreshes sessions and protects routes before any rendering
  3. Server actions handle auth mutations (sign-up, sign-in, sign-out) on the server
  4. getUser() over getSession() for secure auth validation in API routes
  5. RLS integration pushes authorization into the database where it cannot be bypassed

If you are evaluating your backend options, our Firebase vs Supabase comparison breaks down the full platform differences. For cost planning, see our Supabase pricing breakdown with real-world cost scenarios from hobby projects to growth-stage SaaS.

Start with the free tier, follow the patterns in this guide, and ship authentication that scales from your first user to your fifty-thousandth without changing a line of auth code.

Frequently Asked Questions

Yes. Supabase Auth includes a generous free tier with up to 50,000 monthly active users at no cost. The free plan supports email/password login, social auth providers like Google and GitHub, magic links, and phone auth. For most Next.js startups and side projects, the free tier covers the entire early growth phase. When you exceed 50,000 MAUs, the Pro plan at $25 per month includes 100,000 MAUs with additional users at $0.00325 per MAU. This makes Supabase Auth one of the cheapest authentication solutions available for Next.js applications in 2026.

Supabase Auth and Clerk target different needs. Clerk offers pre-built UI components, faster initial setup, and 10,000 free MAUs with a focus on developer experience. Supabase Auth offers 50,000 free MAUs, deep PostgreSQL integration through Row Level Security, open-source transparency, and self-hosting options. Choose Clerk if you want pre-built sign-in forms and multi-tenancy out of the box. Choose Supabase Auth if you already use Supabase for your database, want database-level security through RLS, or need the cost advantage at scale. At 50,000 users, Supabase Auth costs $25 per month compared to approximately $800 per month on Clerk.

Yes. Supabase Auth integrates with Next.js 15 middleware through the @supabase/ssr package. You create a middleware client using createServerClient, call getUser() to validate the session, and redirect unauthenticated users to your login page. The middleware runs on every matching request before it reaches your page components, providing route protection at the edge. This is the recommended approach for protecting routes in Next.js 15 App Router applications because it catches unauthorized requests before any server component rendering begins.

The @supabase/auth-helpers-nextjs package is the legacy library designed for the Next.js Pages Router. The @supabase/ssr package is its modern replacement, optimized for the App Router, React Server Components, and streaming. The ssr package uses createServerClient and createBrowserClient instead of the older createServerSupabaseClient. It handles cookies natively without requiring a React provider component. For any new Next.js 14.2 or newer project, use @supabase/ssr exclusively. The auth-helpers package is no longer actively maintained.

Enable Google as a provider in the Supabase Dashboard under Authentication then Providers. Add your Google Cloud Console OAuth client ID and secret. Set the redirect URL to your Supabase project callback URL. In your Next.js client component, call supabase.auth.signInWithOAuth with provider set to google and a redirectTo option pointing to your auth callback route. Create a callback route handler at app/auth/callback/route.ts that exchanges the authorization code for a session using the PKCE flow. The entire setup takes about 15 minutes.

Yes. Supabase Auth powers production applications for millions of users across thousands of companies. The platform offers 99.9% uptime SLA on Pro plans, SOC 2 compliance, automatic session refresh with JWT rotation, and support for enterprise features like SAML SSO. The auth system integrates directly with PostgreSQL Row Level Security for database-level authorization. For production deployments, use the Pro plan for SLA guarantees, enable email confirmation, configure rate limiting, and follow the Supabase production checklist.

Supabase Auth uses short-lived access tokens (default 1 hour) and long-lived refresh tokens (default 7 days) stored in HTTP-only cookies. The @supabase/ssr package handles token refresh automatically when you call getUser() in middleware or server components. The middleware intercepts every request, checks if the access token is expired, and silently refreshes it using the refresh token before the request reaches your page. On the client side, the Supabase client library handles refresh automatically through the onAuthStateChange listener. No manual refresh logic is needed in most cases.

Create a Supabase server client in your route handler using createServerClient with the cookies from the request. Call getUser() to validate the session. If no user is returned, respond with a 401 status code. For routes that need additional authorization, check user metadata or query your database for role information. Always validate auth server-side in route handlers because client-side checks can be bypassed. For consistent protection across multiple routes, use middleware with a route matcher pattern.

RevKit

Next.js SaaS Starter Kit

Pre-built auth, billing, and dashboard. Launch your SaaS in days, not weeks.

200+ devs shipped with RevKit
Get RevKit
Next.js 15 Supabase Stripe TypeScript

Join 50k+ subscribers

Web dev, SaaS, growth & marketing. Weekly.

Thanks for subscribing! Check your email.

No spam, unsubscribe anytime.