Server-Side Auth With Next.js & Supabase: A Deep Dive
Hey everyone! Ever wondered how to nail server-side authentication in your Next.js apps using Supabase? Well, you're in the right place! We're gonna dive deep into the nitty-gritty, covering everything you need to know to build secure and robust authentication flows. This guide is inspired by the official Supabase documentation (https://supabase.com/docs/guides/auth/server-side/nextjs), but we'll add our own flavor, making sure it's super easy to follow. Get ready to level up your Next.js game with Supabase! We'll be using the Supabase client library. It's awesome because it handles all the heavy lifting of interacting with your Supabase backend. We'll also explore how to use server-side rendering (SSR) to protect your routes and ensure a seamless user experience. Server-side authentication is crucial for a bunch of reasons. First off, it keeps your sensitive data safe because you handle authentication on the server. Secondly, it helps with SEO since search engine crawlers can easily access your content. And lastly, it's generally more secure than client-side authentication. So, let's get started and see how to get this thing working! We'll cover the basics like setting up your project, initializing Supabase, and then move on to implementing the core authentication features such as sign-up, sign-in, and sign-out. We'll also look at how to handle user sessions and protect your routes based on user authentication status. This will be a comprehensive guide, so buckle up, and let's go! I'll be referencing some code examples to help you understand better and make it easier for you to implement it. Trust me, it's not as complex as it might sound! We are going to see how it can be so easy and fun to build. Let's make it awesome, shall we?
Setting Up Your Next.js Project with Supabase
Alright, let's get down to brass tacks and set up our project. First things first, you need a Next.js project. If you don't have one already, no worries, it's a breeze to create one using create-next-app. Open up your terminal and run this command:
npx create-next-app@latest my-supabase-auth-app
cd my-supabase-auth-app
Replace my-supabase-auth-app with whatever you want to name your project. Next, you need to install the Supabase client library. Navigate into your project directory and run:
npm install @supabase/supabase-js
This command installs the Supabase JavaScript client, which we'll use to interact with your Supabase backend. This client library handles the communication with your Supabase project, which includes authentication. Now, you need a Supabase project. If you don't already have one, head over to the Supabase website (https://supabase.com/) and create an account. Once you're logged in, create a new project. You'll need the following from your Supabase project:
- Project URL: You can find this in your Supabase project dashboard, under the settings. It's the URL of your Supabase instance.
- Anon Key: This is also in your project dashboard, under the settings, specifically the API section. This key is used for public operations.
Keep these handy – you'll need them to initialize the Supabase client. Now, create a file called .env.local in the root of your project. This file will store your Supabase credentials. Add the following lines, replacing the placeholders with your actual project URL and anon key:
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_PROJECT_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
Make sure to prefix the variables with NEXT_PUBLIC_ so that they can be accessed in your frontend code. This .env.local file is automatically loaded by Next.js during development. It's important to keep your API keys and secrets secure. Never commit your .env.local file to version control. Now, let's create a utility function to initialize the Supabase client. Create a file called lib/supabase.js (or any other name you prefer) and add the following code:
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
This code initializes the Supabase client using your project URL and anon key from your .env.local file. This client is what you'll use to interact with your Supabase backend. And that's it! Your Next.js project is now set up to use Supabase. You've installed the necessary libraries, created your Supabase project, and initialized the Supabase client. In the following sections, we'll dive into implementing authentication features like signing up, signing in, and signing out.
Implementing Authentication Features
Now that we have our project set up, let's dive into the core of server-side authentication: implementing the actual features. We'll start with the most common ones: signing up, signing in, and signing out. We are going to go through how you can implement these features, using the Supabase client and Next.js's server-side capabilities. Let's make it awesome, shall we?
Sign-Up
Let's implement the sign-up functionality. Create a new file called pages/api/auth/signup.js. This will be an API route that handles user registration. Add the following code:
import { supabase } from '../../../lib/supabase'
export default async function handler(req, res) {
if (req.method === 'POST') {
const { email, password } = req.body
const { data, error } = await supabase.auth.signUp({ email, password })
if (error) {
return res.status(400).json({ error: error.message })
}
return res.status(200).json({ data })
}
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
This API route accepts a POST request with the user's email and password. It then calls supabase.auth.signUp() to register the user. If the sign-up is successful, it returns the user data; otherwise, it returns an error. The supabase.auth.signUp() method handles the creation of a new user in your Supabase database. You can customize the user data (like adding a username) by passing additional parameters to the signUp() method. To make this work, you'll need to create a form in your frontend to collect the user's email and password and then send a POST request to this API route. A form would look something like this in a Next.js component:
import { useState } from 'react'
function SignUpForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
const response = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await response.json()
if (response.ok) {
setSuccess(true)
setError('')
} else {
setError(data.error)
setSuccess(false)
}
}
return (
<form onSubmit={handleSubmit}>
{/* ... your form fields (email, password) ... */}
<button type="submit">Sign Up</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
{success && <p style={{ color: 'green' }}>Successfully signed up!</p>}
</form>
)
}
export default SignUpForm
Sign-In
Next, let's implement the sign-in functionality. Create a new API route called pages/api/auth/signin.js and add the following code:
import { supabase } from '../../../lib/supabase'
export default async function handler(req, res) {
if (req.method === 'POST') {
const { email, password } = req.body
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
if (error) {
return res.status(400).json({ error: error.message })
}
return res.status(200).json({ data })
}
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
This API route handles user login. It takes the user's email and password, then calls supabase.auth.signInWithPassword() to authenticate the user. If successful, it returns user data; otherwise, it returns an error. Just like with sign-up, you'll need a form in your frontend to collect the user's credentials and send a POST request to this API route. The form implementation is similar to the sign-up form. Remember to handle potential errors and display appropriate messages to the user. For instance:
import { useState } from 'react'
function SignInForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
const response = await fetch('/api/auth/signin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const data = await response.json()
if (response.ok) {
setSuccess(true)
setError('')
} else {
setError(data.error)
setSuccess(false)
}
}
return (
<form onSubmit={handleSubmit}>
{/* ... your form fields (email, password) ... */}
<button type="submit">Sign In</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
{success && <p style={{ color: 'green' }}>Successfully signed in!</p>}
</form>
)
}
export default SignInForm
Sign-Out
Lastly, let's implement the sign-out functionality. Create a new API route called pages/api/auth/signout.js and add the following code:
import { supabase } from '../../../lib/supabase'
export default async function handler(req, res) {
if (req.method === 'POST') {
const { error } = await supabase.auth.signOut()
if (error) {
return res.status(400).json({ error: error.message })
}
return res.status(200).json({ message: 'Signed out successfully' })
}
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
}
This API route uses supabase.auth.signOut() to clear the user's session. It's a simple process, usually triggered by a button click. Once the user clicks the sign-out button, a POST request is sent to this API route, and the user's session is terminated. Here's a basic implementation for a sign-out button in your frontend:
function SignOutButton() {
const handleSignOut = async () => {
const response = await fetch('/api/auth/signout', {
method: 'POST',
})
if (response.ok) {
// Redirect or update UI to reflect sign-out
// For example, redirect to the login page or update your app state.
}
}
return <button onClick={handleSignOut}>Sign Out</button>
}
export default SignOutButton
These three API routes – sign-up, sign-in, and sign-out – form the core of your authentication system. Remember to handle errors gracefully and provide a good user experience. Now you have the essential building blocks for user authentication in your Next.js application, which includes how to manage it in the server-side!
Server-Side Rendering and Route Protection
Now that we've covered the basics of authentication, let's talk about server-side rendering (SSR) and route protection. This is where the magic of server-side authentication really shines. SSR allows you to render your pages on the server, which is crucial for protecting routes and providing a secure user experience. With SSR, the server determines whether a user is authenticated before sending the page to the client. This means that unauthenticated users cannot access protected content, and your application is much more secure. Let's dig deeper into how this works in Next.js.
Utilizing getServerSideProps
The cornerstone of server-side rendering in Next.js is the getServerSideProps function. This function runs on the server before the page is rendered and allows you to fetch data, including authentication status. This is how you will determine whether to show the content of your page or redirect the user.
Here’s a basic example:
import { supabase } from '../../lib/supabase'
export async function getServerSideProps(context) {
const { req, res } = context
const { data: { session } } = await supabase.auth.getSession({ req, res })
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
return {
props: {
user: session.user,
},
}
}
function Profile({ user }) {
return (
<div>
<h1>Welcome, {user.email}</h1>
{/* ... your profile content ... */}
</div>
)
}
export default Profile
In this example, getServerSideProps checks for a valid session using supabase.auth.getSession(). If there's no session (meaning the user isn't logged in), it redirects the user to the login page. If there is a session, it passes the user data as props to the component, allowing the profile page to render. In the first few lines of code, you import the Supabase client. After that, we get access to the context to be able to get the session. The getSession method takes your req and res objects from the context and retrieves the session details. Now, if the user does not have a session, we redirect it. If it has a session, we get the user data.
Protecting Routes
Route protection is all about controlling access to your pages based on whether the user is authenticated. Using getServerSideProps, you can easily protect routes. You check for a valid session and redirect unauthenticated users. This keeps your private content private, ensuring that only authenticated users can access them. For example, if you have a dashboard page that you want to protect, you would use getServerSideProps to check for a valid session before rendering the page. If the user is not authenticated, you redirect them to the login page. Using this method, you can keep your data and application safe!
Handling Session Persistence
With server-side authentication, you need to think about session persistence. Supabase automatically handles session persistence by storing the session in a cookie. This cookie is sent with every request from the client, allowing the server to know if the user is authenticated. When the user signs in, Supabase sets a cookie with the session information. When the user signs out, Supabase clears the cookie. The cookies are automatically managed by the browser. If the user closes the browser, the session is cleared, and the user will be asked to sign in again. The getSession method is what you should use to get the session details on the server-side.
Advanced Authentication Techniques
Let's get into some advanced topics to make your authentication setup even more robust and user-friendly. These techniques will provide a better user experience and help you implement a more secure authentication system. We're talking about things like handling email verification, password reset, and even integrating with social login providers. So let's dive in and see how we can make our authentication systems stand out. We will also learn more about how to personalize your user authentication. Let's make it more awesome!
Email Verification
Email verification is a critical step in verifying user identities and preventing abuse. Supabase makes it easy to implement email verification. When a user signs up, you can configure Supabase to send them a verification email. The user must click the link in the email to verify their address. This ensures that the email address belongs to the user and adds an extra layer of security. To enable email verification in Supabase:
- Go to your Supabase project dashboard.
- Click on the