Next.js & Supabase: Seamless Authentication Guide
Master Next.js Supabase Authentication: A Full Guide
Hey guys, let’s dive deep into Next.js Supabase authentication today! If you’re building a web app with Next.js and want a robust, easy-to-implement authentication system, you’ve hit the jackpot. Supabase is an open-source Firebase alternative that offers a PostgreSQL database, authentication, instant APIs, and more. When paired with the power and flexibility of Next.js, you get a development experience that’s both incredibly productive and highly scalable. We’re going to walk through setting up authentication from scratch, covering everything from user sign-up and login to managing user sessions and protecting your routes. By the end of this guide, you’ll have a solid understanding of how to integrate Supabase’s authentication features into your Next.js project, making your app secure and user-friendly. We’ll be focusing on the practical aspects, so get ready to code along and build something awesome! This isn’t just about getting users logged in; it’s about creating a secure and seamless user experience that keeps them coming back. So, whether you’re a seasoned Next.js developer or just starting out, this guide will provide you with the knowledge and tools you need to implement powerful authentication for your applications. We’ll be using the official Supabase JavaScript client library, which makes interacting with your Supabase backend a breeze. We’ll explore different authentication flows, like email/password, social logins, and even magic links, giving you the flexibility to choose what best suits your project needs. Remember, authentication is the gatekeeper of your application, and getting it right is crucial for security and user trust. Let’s get this done!
Table of Contents
Getting Started with Supabase and Next.js
First things first, you need to set up your Supabase project. Head over to
supabase.io
and create a new project. It’s super intuitive – just pick a name, choose a region, and set a password for your
postgres
user. Once your project is ready, you’ll find your
Project URL
and
anon public key
in the API section of your project dashboard. These are crucial credentials you’ll need to connect your Next.js application to your Supabase backend. Now, let’s set up your Next.js project. If you don’t have one already, you can create it using
npx create-next-app@latest my-auth-app
. After creating the app, navigate into your project directory (
cd my-auth-app
). The next step is to install the Supabase JavaScript client:
npm install @supabase/supabase-js
. This library will be our main tool for interacting with Supabase. To manage your Supabase credentials securely, it’s best practice to use environment variables. Create a
.env.local
file in the root of your Next.js project and add your Supabase URL and anon key:
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
Replace
YOUR_SUPABASE_URL
and
YOUR_SUPABASE_ANON_KEY
with the actual values from your Supabase project dashboard. The
NEXT_PUBLIC_
prefix is important because it makes these variables accessible on the client-side in Next.js. Now, let’s create a utility file to initialize the Supabase client. Create a
lib
folder in your
src
directory (or root if you prefer) and then create a
supabaseClient.js
file inside it. This file will export an instance of the Supabase client that you can import and use throughout your application. Here’s the code for
lib/supabaseClient.js
:
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('Supabase URL and anon key are required. Make sure they are set in your .env.local file.');
}
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
This setup ensures that your Supabase client is initialized correctly and ready to be used for all your authentication and database operations. Remember to restart your Next.js development server after adding the
.env.local
file for the environment variables to take effect. This initial setup is critical for establishing a secure and efficient connection between your frontend and backend, laying the groundwork for all subsequent authentication features we’ll implement. It’s all about building a solid foundation, guys!
User Sign-up and Login Flows
Alright, let’s get down to the nitty-gritty: user sign-up and login. This is where the magic of
Next.js Supabase authentication
really shines. We’ll create a simple form for users to sign up using their email and password, and then another for logging in. First, let’s create a new page for our authentication forms, say
app/auth/page.tsx
(if you’re using the App Router) or
pages/auth.js
(if you’re using the Pages Router). We’ll start with the sign-up functionality. We need a form with input fields for email and password, and a button to submit. We’ll use the
supabase.auth.signUp()
method. This method takes an object with
email
and
password
properties. The response from
signUp()
contains user information and an
error
object if something went wrong. Here’s a snippet for a sign-up component:
// Example for src/app/auth/page.tsx (App Router)
'use client';
import { useState } from 'react';
import { supabase } from '../../lib/supabaseClient'; // Adjust path as needed
export default function AuthPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
const { error } = await supabase.auth.signUp({
email,
password,
});
if (error) {
setError(error.message);
console.error('Sign up error:', error);
} else {
// Handle success: maybe redirect to a confirmation page or login page
alert('Sign up successful! Please check your email to confirm your account.');
}
setLoading(false);
};
// ... login form will go here ...
return (
<div>
<h2>Sign Up</h2>
<form onSubmit={handleSignUp}>
<div>
<label htmlFor="email">Email:</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Signing Up...' : 'Sign Up'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
{/* Login form placeholder */}
</div>
);
}
For login, we’ll use
supabase.auth.signInWithPassword()
. It takes a similar object with
email
and
password
. The response structure is also similar, providing user data or an error. Here’s how you can add the login form to the same page:
// Continuing from the AuthPage component above...
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
setError(error.message);
console.error('Login error:', error);
} else {
// Handle success: redirect to dashboard or home page
alert('Login successful!');
window.location.href = '/dashboard'; // Example redirect
}
setLoading(false);
};
return (
<div>
<h2>Sign Up</h2>
<form onSubmit={handleSignUp}>
{/* ... sign up inputs ... */}
</form>
<hr style={{ margin: '20px 0' }} />
<h2>Login</h2>
<form onSubmit={handleLogin}>
<div>
<label htmlFor="login-email">Email:</label>
<input
id="login-email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="login-password">Password:</label>
<input
id="login-password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
</div>
);
}
Notice how we’re reusing the
email
and
password
state for both forms for simplicity in this example. In a real application, you might want separate states or a more sophisticated form management approach. Crucially, Supabase handles email confirmation automatically if you enable it in your Supabase Auth settings. Users will receive an email with a link to verify their email address, which is a standard security practice. This entire process is pretty slick, right? You’ve just implemented user registration and login using just a few lines of code with
Next.js Supabase authentication
!
Managing User Sessions and State
Now that users can sign up and log in, the next crucial piece of the puzzle is managing their
session state
in your Next.js application. You need a way to know
who
is logged in and to persist that information across page navigations and even browser refreshes. Supabase provides a powerful mechanism for this through session management. When a user successfully signs in or signs up, Supabase issues a session token. The Supabase client library automatically stores this token (usually in
localStorage
or
sessionStorage
for client-side auth). We can leverage this to build a global state that reflects the user’s authentication status. A common and highly recommended pattern in Next.js is to use a Context API or a state management library like Zustand or Redux. For simplicity, let’s use React’s Context API to manage the user session globally. First, create a
context
folder (e.g.,
src/context
) and inside it, a
AuthContext.tsx
file:
// src/context/AuthContext.tsx
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { supabase } from '../lib/supabaseClient';
import { User } from '@supabase/supabase-js';
interface AuthContextType {
user: User | null;
session: any | null; // Use a more specific type if available
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const getSession = async () => {
setIsLoading(true);
const { data: { session } } = await supabase.auth.getSession();
setUser(session?.user || null);
setSession(session || null);
setIsLoading(false);
};
getSession();
// Subscribe to authentication changes
const { data: authListener } = supabase.auth.onAuthStateChange(
(_event, session) => {
setUser(session?.user || null);
setSession(session || null);
setIsLoading(false); // Set loading to false once auth state changes
}
);
// Cleanup subscription on unmount
return () => {
authListener?.subscription.unsubscribe();
};
}, []);
return (
<AuthContext.Provider value={{ user, session, isLoading }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
Now, you need to wrap your application with this
AuthProvider
. If you’re using the App Router, modify your root layout (
src/app/layout.tsx
):
// src/app/layout.tsx
import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { AuthProvider } from '../context/AuthContext'; // Adjust path
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'Next.js Supabase Auth',
description: 'Generated by create next app',
};
export default function RootLayout({ children }: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<AuthProvider>
{children}
</AuthProvider>
</body>
</html>
);
}
If you’re using the Pages Router, wrap your
_app.js
file.
With this
AuthProvider
, any component within it can now access the
user
object and
isLoading
state using the
useAuth
hook. The
useEffect
hook fetches the current session when the app loads and subscribes to real-time authentication state changes. This means if a user logs in or out via another tab or device, your application will automatically update. This is key for a smooth
Next.js Supabase authentication
experience. We are keeping track of the user’s status seamlessly!
Protecting Routes with Next.js Middleware
One of the most powerful features for securing your Next.js application is using
middleware
. This allows you to run code
before
a request is completed, enabling you to check authentication status and redirect users if they’re not authorized. This is perfect for protecting pages like a dashboard or user profile. Let’s set up some middleware to protect routes. Create a
middleware.ts
file at the root of your project (alongside
pages
or
app
directory):
// middleware.ts
import { NextResponse, NextRequest } from 'next/server';
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs';
export async function middleware(req: NextRequest) {
const res = NextResponse.next();
// Create a Supabase client configured for middleware
const supabase = createMiddlewareClient({
req,
res,
});
// Refresh the session, which also updates the user object
const { data: { session } } = await supabase.auth.getSession();
// Define protected routes
const protectedRoutes = ['/dashboard', '/profile']; // Add other routes as needed
const isProtectedRoute = protectedRoutes.some(route =>
req.nextUrl.pathname.startsWith(route)
);
if (isProtectedRoute && !session) {
// If it's a protected route and the user is not logged in, redirect to the login page
const url = req.nextUrl.clone();
url.pathname = '/auth'; // Redirect to your auth page
return NextResponse.redirect(url);
}
// If the user is logged in and tries to access the auth page, redirect them to the dashboard
if (session && req.nextUrl.pathname === '/auth') {
const url = req.nextUrl.clone();
url.pathname = '/dashboard';
return NextResponse.redirect(url);
}
return res;
}
// Configure which paths the middleware should run on
export const config = {
matcher: [
/*
* Match all request paths except for:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - .*
*/
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
This middleware uses
@supabase/auth-helpers-nextjs
, a package specifically designed to simplify Supabase integration with Next.js, including middleware. It creates a Supabase client instance using the request and response objects, checks for an active session, and then applies routing logic. If a user tries to access a route listed in
protectedRoutes
without a session, they’re redirected to
/auth
. Conversely, if a logged-in user tries to access
/auth
, they’re redirected to
/dashboard
. The
config.matcher
ensures the middleware runs efficiently by skipping static assets.
To make this work with the App Router, you’ll need to adjust the way you create the Supabase client within the middleware. Instead of
createMiddlewareClient
from
@supabase/auth-helpers-nextjs
, you’ll typically use the official
supabase-js
client and manage cookies manually or use a custom solution. However, for simplicity and broader compatibility,
createMiddlewareClient
is excellent. Ensure you have installed the helper package:
npm install @supabase/auth-helpers-nextjs
.
This is a powerful way to ensure that only authenticated users can access sensitive parts of your application, greatly enhancing the security and user experience. Next.js Supabase authentication is truly robust when combined with middleware!
Handling Logout
Logging out a user is just as important as logging them in. It securely ends their session and clears their authentication state. Supabase makes this straightforward. We’ll add a logout button, likely in a navigation bar or user profile component, that triggers the logout action.
First, let’s create a simple logout function. You can place this in your
AuthContext.tsx
or create a separate utility file.
// In AuthContext.tsx or a utility file
// ... existing AuthProvider code ...
const logout = async () => {
setIsLoading(true); // Optional: show a loading indicator
const { error } = await supabase.auth.signOut();
if (error) {
console.error('Logout error:', error);
// Handle error, e.g., show a message to the user
} else {
// Session is cleared automatically by supabase client on successful signOut
// The onAuthStateChange listener will update the user and session states
alert('You have been logged out.');
}
setIsLoading(false);
};
return (
<AuthContext.Provider value={{ user, session, isLoading, logout }}>
{children}
</AuthContext.Provider>
);
// ... rest of AuthContext.tsx
// Update useAuth hook to include logout
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
Now, in any component where you use the
useAuth
hook, you can access and call the
logout
function. For example, in a header component:
// Example: src/components/Header.tsx
'use client';
import Link from 'next/link';
import { useAuth } from '../context/AuthContext'; // Adjust path
export default function Header() {
const { user, logout } = useAuth();
return (
<header>
<nav>
<Link href="/">Home</Link>
{' '}
{user ? (
<>
<Link href="/dashboard">Dashboard</Link>
{' '}
<button onClick={logout as any}>Logout</button>
</>
) : (
<Link href="/auth">Login / Sign Up</Link>
)}
</nav>
</header>
);
}
Remember to import and render this
Header
component in your root layout (
layout.tsx
or
_app.js
). When the logout button is clicked,
supabase.auth.signOut()
is called. This invalidates the user’s session on the server and, importantly, the
onAuthStateChange
listener within our
AuthProvider
will detect this change and update the
user
and
session
state to
null
. This automatically reflects the logged-out state across your entire application. You can then conditionally render UI elements based on the
user
object being
null
or not. This seamless handling of logout is a core part of a great
Next.js Supabase authentication
system.
Advanced: Social Logins and Magic Links
Supabase offers more than just email and password authentication. Let’s touch upon social logins (like Google, GitHub, etc.) and magic links . These can significantly improve user experience by reducing friction.
Social Logins:
To enable social logins, you first need to configure the desired providers in your Supabase project settings under Authentication > Authentication Providers. Once enabled, you can use the
supabase.auth.signInWithOAuth()
method. This method takes an object with the
provider
name (e.g.,
'google'
,
'github'
) and a
options
object which can include a
redirectTo
URL.
// Example login button component
'use client';
import { supabase } from '../lib/supabaseClient';
const SocialLogin = ({ provider }) => {
const handleSocialLogin = async () => {
const { error } = await supabase.auth.signInWithOAuth({
provider: provider,
options: {
// Important: This URL must be added to your "Redirect URLs" in Supabase Auth settings
redirectTo: `${window.location.origin}/auth/callback`
}
});
if (error) {
console.error('Social login error:', error);
}
};
return (
<button onClick={handleSocialLogin}>
Continue with {provider.charAt(0).toUpperCase() + provider.slice(1)}
</button>
);
};
export default SocialLogin;
After the user authenticates with the social provider, they will be redirected back to the
redirectTo
URL. You’ll need a page (e.g.,
/auth/callback
) to handle this redirect and exchange the authorization code for a session. The
@supabase/auth-helpers-nextjs
package provides utilities for this, but you can also handle it manually using
supabase.auth.exchangeCodeForSession()
.
Magic Links:
Magic links allow users to log in by clicking a link sent to their email, bypassing the need for a password. To use them, you’ll call
supabase.auth.signInWithMagicLink()
.
// Example for magic link sign-in/sign-up form
'use client';
import { useState } from 'react';
import { supabase } from '../lib/supabaseClient';
const MagicLinkForm = () => {
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [error, setError] = useState('');
const handleMagicLink = async (e) => {
e.preventDefault();
setMessage('');
setError('');
const { error } = await supabase.auth.signInWithMagicLink({
email,
options: {
// Optional: Redirect after successful email confirmation
redirectTo: `${window.location.origin}/dashboard`
}
});
if (error) {
setError(error.message);
} else {
setMessage('Check your email for the magic link!');
}
};
return (
<form onSubmit={handleMagicLink}>
<h2>Login with Magic Link</h2>
<input
type="email"
placeholder="Your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<button type="submit">Send Magic Link</button>
{message && <p>{message}</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
</form>
);
};
export default MagicLinkForm;
Again, ensure the
redirectTo
URLs are registered in your Supabase project settings. These advanced features add significant flexibility to your
Next.js Supabase authentication
strategy, catering to different user preferences and enhancing accessibility. It’s all about making it easy and secure for your users!
Conclusion
And there you have it, folks! We’ve covered the essential steps to implement robust Next.js Supabase authentication . From setting up your Supabase project and Next.js app, handling user sign-up and login, managing session state globally with context, to protecting your routes with middleware and enabling logout – you’re now equipped with a solid foundation. Supabase and Next.js are a powerful combination, offering a developer-friendly experience with built-in security. Remember to always keep your API keys secure using environment variables and to configure your redirect URLs properly in Supabase for social logins and magic links. The flexibility Supabase offers, especially with its real-time capabilities and extensive auth providers, makes it an excellent choice for modern web applications. Keep experimenting, keep building, and happy coding! You’ve got this, guys!