Next.js Authentication with Supabase and NextAuth.js: A Deep Dive (Part 1 of 3) | AI Hub Blog | AI Hub
Tutorial
Next.js Authentication with Supabase and NextAuth.js: A Deep Dive (Part 1 of 3)
S
Sidharrth Mahadevan
AI Research Lead
February 22, 2025
11 min read
Generated by AI-Hub
Build a secure, modern, production-grade authentication flow using Next.js, NextAuth.js, and Supabase. Learn the core architecture, configuration options, and integration best practices in this comprehensive guide.
Next.js Authentication with Supabase and NextAuth.js: A Deep Dive (Part 1 of 3)
By Sidharrth Mahadevan | Published: February 22, 2025
Category: Tutorial
Introduction
Implementing a bulletproof authentication flow is one of the most critical aspects of modern web development. While standard SaaS identity solutions (like Clerk or Auth0) provide rapid setup, they can become prohibitively expensive as your active user base grows. On the other hand, rolling an entire database-level auth mechanism from scratch is famously error-prone, raising significant risks of Cross-Site Scripting (XSS), SQL injection, and session hijacking.
This is where the pairing of NextAuth.js (Auth.js) and Supabase shines. Together, they offer an elegant hybrid: you retain complete, low-cost ownership of your PostgreSQL database through Supabase, while NextAuth.js handles OAuth configurations, secure cookies, and React-friendly session hooks.
Deepen Your Knowledge
Continue exploring related insights and research in Tutorial.
Get curated tutorials, tool comparisons, and industry news delivered directly to your inbox. No spam, ever.
By subscribing, you agree to our Terms of Service and Privacy Policy.
Welcome to the first installment of our three-part series on Next.js authentication using Supabase and NextAuth.js. In this foundational guide, we will design the architecture, configure our development environments, build a resilient Supabase connection client, and write the core NextAuth.js dynamic handlers.
Series Roadmap
Part 1: Architecture design, dependency setup, environment hardening, and dynamic auth routing (You are here! π)
Part 2: Complete Sign-Up and Sign-In pipelines, session syncing, and Route Protection Middleware
Part 3: Advanced flows (Password recovery, profile updates, multi-factor authentication) and production security hardening
π€ Why NextAuth.js & Supabase? An Architectural Comparison
To understand why this tech stack is so powerful, let's analyze how it stacks up against alternative patterns:
Feature
NextAuth.js + Supabase
Supabase Auth (Native)
NextAuth + Direct Prisma/SQL
Session Mechanism
Server-side JWT (JWE) or Session Store
JWTs managed by GoTrue engine
Database-backed Sessions / Local JWT
OAuth Provider Reach
80+ pre-built adapters
Limited to Supabase integrations
80+ pre-built adapters
Vendor Lock-in
Low (decoupled logic)
High (dependent on GoTrue)
Very Low (pure DB)
Security Footprint
HttpOnly, Secure, SameSite cookies
Browser localStorage / Cookie Middleware
HttpOnly, Secure, SameSite cookies
Development Speed
Fast (Zero-config dynamic routes)
Very Fast
Slow (manual DB schema generation)
Architectural Synthesis
NextAuth.js: Functions as your application's identity manager. It acts as the gateway for users logging in, coordinates social providers (Google, GitHub, Apple), handles session expiration, and packages user profiles in a encrypted JSON Web Token (JWT) securely stored in HttpOnly cookies (inaccessible to malicious client-side scripts).
Supabase: Serves as your backend persistence engine. It provides a managed PostgreSQL instance, handles Row-Level Security (RLS) policies, and runs custom business logic via database triggers.
By leveraging this decoupled approach, NextAuth.js manages your user session cookies safely in the browser, while your backend logic uses a secure server-to-server connection to query and write data to Supabase.
1. ποΈ Project Setup & Installation
To get started, spin up a new Next.js project using your package manager of choice. For this tutorial, we're focusing heavily on the modern Next.js App Router (Next.js 13, 14, and 15), but we have also documented compatibility paths for the legacy Pages Router.
1.1 Package Installation
In your terminal, navigate to your project root and install the core dependencies:
npm install next-auth @supabase/supabase-js
Next, install the required TypeScript declarations as dev dependencies to ensure type safety throughout our auth lifecycle:
npm install --save-dev @types/node @types/react
1.2 Defining the Project Structure
Organizing your directory structure early is critical to maintaining a clean codebase. Below is the production-ready directory tree demonstrating where our components, database interfaces, and route handlers will live.
(a) Next.js 13+ (App Router Structure)
For standard App Router projects, write your dynamic auth handler inside the app/api/auth/[...nextauth] route directory. This acts as a catch-all route handler for all HTTP requests to /api/auth/*.
Why [...nextauth]? The dynamic catch-all route name is highly deliberate. NextAuth.js internally mounts multiple sub-routes on top of this pathβsuch as /api/auth/providers, /api/auth/signin, /api/auth/signout, and /api/auth/callback. By defining a catch-all route, NextAuth automatically routes all incoming requests to their respective controllers under the hood.
(b) Next.js 12 or Earlier (Pages Router Structure)
If you are maintaining a legacy codebase utilizing the Pages Router, map the setup directly to your pages/api/ folder:
Assign your project an intuitive name, select your closest AWS/GCP region, and generate a secure database password.
Once initialization finishes, navigate to Project Settings -> API on the sidebar.
Locate and copy both the Project URL and the Anon / Public API Key.
1.4 Hardening Environment Variables
Environment variables dictate how our applications verify authorization and sign data. Create a .env.local file in your project's root directory and map your configuration keys precisely:
NEXTAUTH_URL: Denotes the canonical base URL of your application. During local development, this is http://localhost:3000. In staging or production, replace this with your target domain (e.g., https://auth-hub.com).
NEXTAUTH_SECRET: Used by NextAuth to sign and encrypt session cookies and JWT payloads. Do not use a weak placeholder. Generate a highly secure, cryptographic secret by running this command in your local terminal:
openssl rand -base64 32
NEXT_PUBLIC_SUPABASE_URL: Points the Supabase SDK to your dedicated cloud instance.
NEXT_PUBLIC_SUPABASE_ANON_KEY: The public anonymous key. It is safe to expose to browsers because it operates inside the constraints of PostgreSQL's Row-Level Security (RLS) system. However, ensure that write and read privileges on sensitive user tables are protected by policies.
π‘ Developer Pro-Tip: To prevent silent configuration bugs in production, validate these environment variables at application startup. You can write a tiny validation utility using a schema parser like Zod or construct inline assertions inside your server configurations to throw descriptive compile-time errors if a variable is missing.
2. Building the Auth Foundation
With our directory layout and env keys locked down, we are ready to write our integration layer.
2.1 π Initializing a Resilient Supabase Client
Create a database client module inside your helper library folder (lib/supabase.ts). This client allows your application servers to interact cleanly with Supabase's auth and database engines:
import { createClient } from '@supabase/supabase-js';
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new Error(
'CRITICAL: Missing Supabase environment variables. Confirm NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY are present inside your .env configuration file.'
);
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: false, // Prevents Supabase client from caching local tokens in SSR environments
autoRefreshToken: false, // Hands complete session lifetime control over to NextAuth.js
},
});
βοΈ Why these specific options?
Setting persistSession: false and autoRefreshToken: false is a crucial best practice when combining Supabase with NextAuth.js. By default, the Supabase client attempts to write auth states directly to localStorage or browser cookies.
Because we are designating NextAuth.js as our primary source of truth for user session state, letting the Supabase client independently cache tokens can cause sync mismatches, where NextAuth thinks a user is signed out, but the Supabase client thinks they are still logged in. Disabling these options keeps our session lifecycle fully aligned under NextAuth.
2.2 π― Drafting the NextAuth Core Configuration
Now, we'll draft our dynamic route handler. Create the route handler file at /app/api/auth/[...nextauth]/route.ts to coordinate credential checking, JWT serialization, and redirection logic.
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export const authOptions: NextAuthOptions = {
providers: [
CredentialsProvider({
name: 'Supabase Credentials',
credentials: {
email: { label: "Email Address", type: "email", placeholder: "you@example.com" },
password: { label: "Password", type: "password" },
mode: { label: "Mode", type: "text" }
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Please enter both your email address and password.');
}
// Note: The complete credentials validation pipeline with Supabase
// integration will be fully realized in Part 2 of this series.
// We currently return null to prevent unconfigured access.
return null;
}
})
],
session: {
strategy: 'jwt',
maxAge: 30 * 24 * 60 * 60, // 30 Days before cookie expiry
},
pages: {
signIn: '/auth/signin',
error: '/auth/error',
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.email = user.email;
}
return token;
},
async session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
}
return session;
}
},
secret: process.env.NEXTAUTH_SECRET,
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
π΅οΈ Code Walkthrough and Architecture Analysis
Let's break down the mechanics of the code block above:
CredentialsProvider: This instructs NextAuth.js to skip automated third-party OAuth callbacks and trust our custom form logic instead. In Part 2, we'll hook up our standard sign-in interface directly to this module.
session.strategy: 'jwt': By opting for JWT strategy, we avoid sending high-frequency round-trip database queries to resolve user identities during standard application routing. Instead, sessions are evaluated via encrypted tokens signed by our private NEXTAUTH_SECRET key.
pages Configuration Mapping: This overrides NextAuth's unstyled, generic default authentication templates and maps them to our own pages inside /auth/signin and /auth/error. This is critical for matching your project's brand design.
jwt and session Callbacks: These act as an assembly line. When a user successfully authenticates, their record is passed to the jwt callback, which appends custom identifiers (such as database UUIDs) to the token. Next, the session callback maps those token keys over to the accessible session object, making them available to React contexts via the useSession() hook.
Dynamic Export (handler as GET, handler as POST): In the Next.js App Router, routes export HTTP verb functions. Because authentication protocols use GET (for resolving sessions, metadata, and sign-out calls) and POST (for authorizing credentials and handling webhooks), we must export our NextAuth instance as both HTTP verbs.
π οΈ Security Best Practices for Local Development
As you begin developing, build these core security principles into your authentication architecture:
Enforce SameSite Cookie Integrity: Ensure your deployment platform serves cookies with a SameSite=Lax or SameSite=Strict flag to prevent Cross-Site Request Forgery (CSRF) exploits.
HTTPS Everywhere: Even on local hosts, configure your routing server to load HTTPS configurations. On production servers, never transmit data over unencrypted HTTP, as cookies passing through HTTP can be easily intercepted.
Validate JWT Payload Limits: Keep standard key metadata in your JWT sessions compact. Standard web browser cookie limits restrict storage to roughly 4KB. If you must track complex metadata or application states, map those keys within your Postgres user tables instead of packaging them inside the token itself.
β FAQ
Q1: Can I use both standard Credentials provider and Google/GitHub OAuth providers together?
Absolutely. You can add standard OAuth providers alongside your custom credentials configuration within the providers array. NextAuth.js will handle them in parallel automatically:
import GoogleProvider from 'next-auth/providers/google';
// Add to providers list inside NextAuthOptions
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!
})
We will explore user database sync integrations for dual logins in detail in Part 3.
Q2: Why should I write a custom authentication UI instead of relying on Supabase's pre-built UI components?
While the official @supabase/auth-ui-react component library is highly convenient for prototyping, relying on it ties your site design directly to Supabase's styling configurations and architecture. Setting up a decoupled UI using NextAuth.js gives you full styling control with tools like Tailwind CSS and lets you quickly update your identity providers down the road without rewriting your frontend interface.
Q3: Why not use Database Adapters directly with Supabase?
Database adapters work great for storing active user sessions inside your own database tables. However, this generates higher database overhead. Using a JWT Session Strategy eliminates the need for database queries on every page reload, letting your database focus on application-specific queries instead of routing lookups.
π Previewing Part 2 of Our Series
Now that our base setup is fully configured, we're ready to write our core authentication logic. In Part 2, we will:
Establish the Sign-Up Route: Securely hash credentials and write new user records to Supabase.
Protect App Routes: Create a robust middleware file that instantly intercepts unauthenticated users before they can access dashboard routes.
π Conclusion
We've laid a strong foundation for our authentication flow:
Initialized and structured a Next.js App Router project framework.
Wired up a single-instance database client that works cleanly with serverless environments.
Created a scalable, secure NextAuth configuration that maps custom page routes and custom JWT payloads.
Get ready for Part 2! If you ran into any issues or have questions about the setup, drop a comment belowβand we'll see you in the next tutorial! π
How to Use AI for Content Creation: Best Practices
Discover how to leverage AI for content creation without losing your brand's voice or hurting your SEO. This comprehensive guide covers the Human-in-the-Loop workflow, Python integration, style prompts, and E-E-A-T compliance.