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

> A complete supabase auth nextjs guide covering email/password authentication, Google and GitHub social login, magic links, middleware route protection, server-side auth with createServerClient, and Auth.js comparison. Production-ready code examples for Next.js 15 App Router.

Source: https://designrevision.com/blog/supabase-auth-nextjs

---

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](/blog/supabase-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](/blog/supabase-row-level-security) for database-level authorization that no API bypass can circumvent
> * For a ready-made Next.js + Supabase Auth stack, [RevKit](https://revkit.dev) ships with authentication, billing, and admin tooling pre-configured

## Table of Contents

1. [Project Setup](#project-setup)
2. [Creating Supabase Clients for Next.js 15](#creating-supabase-clients-for-nextjs-15)
3. [Email and Password Authentication](#email-and-password-authentication)
4. [Social Authentication: Google, GitHub, and Apple](#social-authentication-google-github-and-apple)
5. [Magic Link Authentication](#magic-link-authentication)
6. [Middleware Route Protection](#middleware-route-protection)
7. [Server-Side Auth in React Server Components](#server-side-auth-in-react-server-components)
8. [Protecting API Routes](#protecting-api-routes)
9. [Auth Callback Route Handler](#auth-callback-route-handler)
10. [Session Management and Refresh](#session-management-and-refresh)
11. [Supabase Auth vs Auth.js: When to Use Which](#supabase-auth-vs-authjs-when-to-use-which)
12. [Production Checklist](#production-checklist)
13. [Conclusion](#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.

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

Add your Supabase credentials to `.env.local`:

```env
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](/blog/supabase-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:

```typescript
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:

```typescript
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:

```typescript
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

```typescript
// 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

```typescript
// 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

```tsx
// 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

```typescript
// 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 <a href="https://console.cloud.google.com/" rel="nofollow" target="_blank">Google Cloud Console</a>. Add `http://localhost:3000` to authorized origins for development.

For GitHub: Create an OAuth App in <a href="https://github.com/settings/developers" rel="nofollow" target="_blank">GitHub Developer Settings</a>. Set the callback URL to your Supabase project callback.

For Apple: Configure Sign in with Apple in the <a href="https://developer.apple.com/" rel="nofollow" target="_blank">Apple Developer Portal</a>. 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:

```tsx
// 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](#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:

```typescript
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

```typescript
// 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

```tsx
// 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](#auth-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:

```typescript
// 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](#middleware-client)) 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:

```typescript
// 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

```tsx
// 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](/blog/supabase-row-level-security), your database queries automatically filter data based on the authenticated user. No manual `WHERE user_id = ?` clauses needed.

```tsx
// 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](/blog/supabase-row-level-security).

### Auth in Server Actions

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

```typescript
// 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.

```typescript
// 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.

```typescript
// 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:

```tsx
'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](/blog/auth-providers-compared). If you are building a SaaS and want broader implementation patterns, our [SaaS authentication guide](/blog/saas-authentication-guide) covers multi-tenant auth, RBAC, and security best practices.

### What About Clerk?

[Clerk](/blog/auth-providers-compared) 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](/blog/supabase-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](/blog/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](/blog/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](https://revkit.dev) exists.

[RevKit](https://revkit.dev) 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 <a href="https://ui.shadcn.com/" rel="nofollow" target="_blank">shadcn/ui</a> 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](/blog/shipfast-vs-supastarter-vs-revkit).

## 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](/blog/supabase-vs-firebase) breaks down the full platform differences. For cost planning, see our [Supabase pricing breakdown](/blog/supabase-pricing) 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.
