Next.js + Supabase Authentication: The Complete 2026 Guide
Learn how to implement email, OAuth, magic links, and session management with Next.js and Supabase. Covers middleware-based route protection, token refresh, and common pitfalls.
On this page
- Why Authentication Is the Hardest Part of Starting a SaaS
- Setting Up Supabase Auth
- Create Your Supabase Project
- Install the Dependencies
- Client Setup for the App Router
- Browser Client
- Server Client
- Implementing Email + Password Auth
- Sign Up
- Sign In
- Common Pitfall: Email Confirmation
- Adding OAuth Providers
- Google OAuth
- GitHub OAuth
- The Auth Callback Route
- Magic Links
- Middleware-Based Route Protection
- Why getUser() Instead of getSession()?
- Session Management Across Tabs
- Security Considerations
- Rate Limiting
- Row Level Security
- Related Guides
- Skip the Setup, Start Building
Launch your SaaS in 30 minutes with production-ready auth, payments, monitoring, and deployment. $49 one-time.
Get Instant AccessWhy Authentication Is the Hardest Part of Starting a SaaS
Every SaaS needs authentication. And every developer who's built it from scratch knows it's deceptively complex. The login form is the easy part. The hard part is everything else:
- Secure token storage and refresh
- OAuth callback handling across providers
- Email verification flows
- Password reset with expiring tokens
- Session management across tabs
- Middleware-based route protection
- CSRF protection
- Rate limiting on auth endpoints
This guide covers how to implement all of it with Next.js and Supabase — and where the common pitfalls are. If you'd rather skip the setup entirely, see our 30-minute Next.js + Supabase launch guide.
Setting Up Supabase Auth
Create Your Supabase Project
Head to supabase.com and create a new project. You'll need:
- Project URL:
https://your-project.supabase.co - Anon Key: The public key for client-side operations
- Service Role Key: The secret key for server-side operations (never expose this)
Install the Dependencies
npm install @supabase/supabase-js @supabase/ssr
The @supabase/ssr package is critical — it handles cookie-based auth that works with Next.js server components and middleware.
Client Setup for the App Router
You need two separate Supabase clients: one for the browser, one for the server.
Browser Client
The browser client runs in client components and uses cookies to manage the session:
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
The server client reads cookies from the request to reconstruct the session:
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createServerSupabaseClient() {
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) {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
},
},
}
);
}
Implementing Email + Password Auth
Sign Up
const { data, error } = await supabase.auth.signUp({
email: 'user@example.com',
password: 'securepassword123',
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
},
});
The emailRedirectTo is crucial — it's where Supabase sends the user after they click the verification link.
Sign In
const { data, error } = await supabase.auth.signInWithPassword({
email: 'user@example.com',
password: 'securepassword123',
});
Common Pitfall: Email Confirmation
By default, Supabase requires email confirmation. If you're testing locally and emails aren't arriving, check the Supabase dashboard under Authentication > Settings. You can disable email confirmation for development, but always keep it enabled in production.
Adding OAuth Providers
Google OAuth
- Create credentials in the Google Cloud Console
- Add the client ID and secret in Supabase Dashboard > Authentication > Providers > Google
- Set the redirect URL to
https://your-project.supabase.co/auth/v1/callback
Then in your code:
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
GitHub OAuth
Same pattern — create an OAuth app in GitHub Settings, add credentials to Supabase, then:
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
The Auth Callback Route
Every OAuth flow needs a callback route to exchange the code for a session:
// app/auth/callback/route.ts
import { createServerSupabaseClient } from '@/lib/supabase-server';
import { NextResponse } from 'next/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 createServerSupabaseClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
}
return NextResponse.redirect(`${origin}/auth/error`);
}
Magic Links
Magic links let users sign in without a password — they click a link in their email and they're in:
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
},
});
This is increasingly popular for SaaS products because it removes password friction entirely.
Middleware-Based Route Protection
This is where most tutorials fall short. You need middleware that:
- Refreshes the session token on every request
- Redirects unauthenticated users away from protected routes
- Redirects authenticated users away from auth pages
// middleware.ts
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(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 }) =>
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();
// Protect dashboard routes
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const url = request.nextUrl.clone();
url.pathname = '/login';
return NextResponse.redirect(url);
}
return supabaseResponse;
}
export const config = {
matcher: ['/dashboard/:path*', '/account/:path*'],
};
Why getUser() Instead of getSession()?
getSession() reads from the local JWT without verifying it with Supabase. getUser() makes a network call to verify the token is still valid. In middleware, always use getUser() — it catches revoked sessions and expired tokens.
Stop wiring this up by hand. SaaSInMinutes ships the full auth stack — email + password, Google and GitHub OAuth, magic links, middleware protection, session refresh, and rate limiting — already wired and tested. $49 one-time. Get instant access →
Session Management Across Tabs
Supabase handles this automatically via its onAuthStateChange listener. Set it up once in your root layout:
'use client';
import { useEffect } from 'react';
import { createClient } from '@/lib/supabase-browser';
import { useRouter } from 'next/navigation';
export function AuthListener() {
const router = useRouter();
const supabase = createClient();
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event) => {
if (event === 'SIGNED_OUT') {
router.push('/login');
}
router.refresh();
}
);
return () => subscription.unsubscribe();
}, [router, supabase]);
return null;
}
Security Considerations
Rate Limiting
Supabase applies rate limiting on auth endpoints by default, but you should add your own application-level rate limiting for login attempts. A simple approach using an in-memory store works for single-server deployments:
const loginAttempts = new Map<string, { count: number; resetAt: number }>();
function isRateLimited(ip: string): boolean {
const now = Date.now();
const record = loginAttempts.get(ip);
if (!record || now > record.resetAt) {
loginAttempts.set(ip, { count: 1, resetAt: now + 900000 }); // 15 min window
return false;
}
record.count++;
return record.count > 5; // Max 5 attempts per 15 minutes
}
Row Level Security
Your Supabase database should use Row Level Security (RLS) policies to ensure users can only access their own data. This is your last line of defense if a token is compromised:
CREATE POLICY "Users can only read own data"
ON profiles FOR SELECT
USING (auth.uid() = id);
For multi-tenant SaaS — where users belong to organizations and need access to shared org data — RLS gets significantly more nuanced. See our deep dive on Supabase RLS for multi-tenant SaaS for organization-based policies, role hierarchies, and the performance tradeoffs.
Related Guides
- Supabase RLS for Multi-Tenant SaaS — once auth is done, this is how you lock down the data layer
- LemonSqueezy Payment Integration for Next.js — the next thing every SaaS needs after auth
- Deploy Next.js on AWS EC2 + S3 + CloudFront — production deployment without Vercel lock-in
- Launch a SaaS in 30 Minutes — the full zero-to-production walkthrough
Skip the Setup, Start Building
Reading through this guide gives you a sense of the surface area — and we haven't even covered password reset flows, email templates, account deletion, or the dozen small things that break in production. Wiring it all up cleanly takes most teams 15–25 hours.
SaaSInMinutes ships it done. For $49 one-time, you get:
- Email + password auth with verification and password reset
- Google and GitHub OAuth, pre-configured
- Magic links
- Middleware-based route protection with token refresh
- Session management and
onAuthStateChangewiring - Rate limiting on auth endpoints
- RLS policies and tested security defaults
- Plus payments, monitoring, email, and production AWS deployment
Spend your time on the features that make your SaaS unique, not on login forms.
Written by SaaSInMinutes