Supabase Auth With Next.js: A Step-by-Step Guide

by Jhon Lennon 49 views

Hey guys! If you're diving into building awesome apps with Next.js and want to add secure authentication, you've come to the right place. Today, we're going to walk through a Supabase Auth Next.js tutorial that will get you up and running with user sign-ups, logins, and even secure routing in no time. Supabase is a game-changer, offering a Firebase-like experience but with the power and flexibility of PostgreSQL. And when you pair it with Next.js, a fantastic React framework, you're setting yourself up for success. So, grab your favorite beverage, and let's get this authentication party started!

Setting Up Your Supabase Project

First things first, getting your Supabase project set up is crucial for this Supabase Auth Next.js tutorial. Head over to Supabase.io and sign up for a free account if you haven't already. Once you're logged in, click on "New Project" and give it a cool name. You'll also need to choose a region and set a password for your postgres user. Don't lose that password, guys! After a minute or two, your project will be ready. You'll be greeted with your project dashboard, which is your command center for everything Supabase. The most important piece of information you'll need right now is your Project URL and anon public key. You can find these on the API page within your project settings. Keep these handy, as we'll be using them to connect our Next.js application to Supabase. We'll also be setting up a simple table later on to demonstrate how to protect routes based on authentication status, so make sure you're familiar with the "Table Editor" section. It's super intuitive, so don't stress about it. The beauty of Supabase is how quickly you can get a backend up and running without needing to manage servers yourself. For this tutorial, we're focusing purely on authentication, but remember that Supabase offers so much more, like real-time subscriptions, storage, and edge functions. So, while we're setting up, take a moment to explore the dashboard and get a feel for it. The more comfortable you are with the Supabase ecosystem, the smoother this entire process will be. We're going to be using environment variables to store our Supabase credentials, which is a best practice for security. So, make sure you create a .env.local file in the root of your Next.js project and add your NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY there. This keeps your sensitive information out of your code repository. It’s a small step, but a super important one for any real-world application you build.

Integrating Supabase with Your Next.js App

Now that your Supabase project is ready, let's integrate Supabase with your Next.js app. If you don't have a Next.js project yet, create one using npx create-next-app@latest my-supabase-app. Once inside your project directory, install the Supabase client library: npm install @supabase/supabase-js. This library is our bridge to interact with your Supabase backend. The next step is to create a Supabase client instance. In the root of your src folder (or wherever you prefer to keep your utility files), create a new file called supabaseClient.js. Inside this file, you'll import the createClient function from @supabase/supabase-js and initialize your client using the environment variables we set up earlier. It should look something like this:

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 supabaseClient.js file will be imported wherever you need to interact with Supabase. It's good practice to keep this setup in one place to maintain consistency. We're making these environment variables public (NEXT_PUBLIC_) because the Supabase client-side library needs them to connect. For server-side operations, you'd use a service key, but for authentication handled on the client, the anon key is sufficient and necessary. Think of this as creating a secure, yet accessible, channel for your frontend to talk to your Supabase backend. We're not exposing any sensitive secrets here; just the public URL and the anonymous authentication key, which is designed for client-side access. This approach ensures that your database credentials or any other highly sensitive information remain hidden on your server. The createClient function handles all the underlying HTTP requests and WebSocket connections needed to interact with Supabase services. So, with this simple setup, your Next.js application is now officially connected to your Supabase project, ready to handle user authentication and other backend tasks. It's amazing how little code it takes to establish such a powerful connection, right? This foundational step is critical for everything that follows in our Supabase Auth Next.js tutorial.

Building the Sign-Up and Login Forms

Alright, let's get to the fun part: building the sign-up and login forms! This is where we'll start using the Supabase client library to handle user interactions. First, create a new page in your pages/auth directory (if you don't have one) called login.js. This page will house both our sign-up and login forms. We'll use React state to manage the email and password inputs, and we'll add buttons to trigger the sign-up and login actions. Here's a basic structure:

import { useState } from 'react';
import { supabase } from '../supabaseClient';

export default function LoginPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleLogin = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    const { error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });

    if (error) setError(error.message);
    setLoading(false);
    // Redirect to dashboard or home page on success
  };

  const handleSignUp = async (e) => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    const { error } = await supabase.auth.signUp({
      email,
      password,
    });

    if (error) setError(error.message);
    setLoading(false);
    // Show success message or redirect
  };

  return (
    <div>
      <h1>Welcome!</h1>
      <form onSubmit={handleLogin}>
        <h2>Login</h2>
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
        <input
          type="password"
          placeholder="Password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
        <button type="submit" disabled={loading}>
          {loading ? 'Logging in...' : 'Login'}
        </button>
        {error && <p style={{ color: 'red' }}>{error}</p>}
      </form>

      <form onSubmit={handleSignUp}>
        <h2>Sign Up</h2>
        <input
          type="email"
          placeholder="Email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
        <input
          type="password"
          placeholder="Password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
        <button type="submit" disabled={loading}>
          {loading ? 'Signing up...' : 'Sign Up'}
        </button>
        {error && <p style={{ color: 'red' }}>{error}</p>}
      </form>
    </div>
  );
}

In this code, we're using useState to manage the form inputs and loading/error states. The handleLogin function uses supabase.auth.signInWithPassword, and handleSignUp uses supabase.auth.signUp. Both functions are async and handle potential errors. Remember to replace the comment about redirection with actual navigation logic using useRouter from next/router once the authentication is successful. We'll cover redirection more in the next section. The key takeaway here is how straightforward it is to trigger authentication flows with the Supabase client. You provide the email and password, and Supabase handles the rest, including password hashing, email verification (which you can configure in your Supabase project settings), and session management. For now, we're just logging errors, but in a production app, you'd want to provide more user-friendly feedback. Also, notice the disabled={loading} attribute on the buttons to prevent multiple submissions while the request is in progress. This is a small UX improvement that makes a big difference. We've also kept the sign-up and login forms relatively simple, but you could easily add more fields like username or first name depending on your needs. Supabase's auth system is quite flexible, allowing you to add custom metadata to user profiles. Don't forget to configure email confirmation in your Supabase project settings if you want to ensure users have valid email addresses before they can fully log in. This is a standard security practice. So, you've got the basic building blocks for user authentication right here in this Supabase Auth Next.js tutorial. Pretty neat, huh?

Managing User Sessions and Protected Routes

Now that users can sign up and log in, we need a way to manage user sessions and protect routes. Supabase makes this incredibly easy. The supabase client automatically manages the session. When a user logs in, Supabase sets a session cookie, and the client library uses this to keep the user authenticated across requests. To check the current user's authentication status, you can use supabase.auth.getUser(). This is crucial for deciding whether to show certain content or redirect users.

Let's create a simple way to get the user session on the client side. You can create a custom hook, say useUser.js:

import { useState, useEffect } from 'react';
import { supabase } from '../supabaseClient';

export const useUser = () => {
  const [user, setUser] = useState(null);
  const [session, setSession] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchUser = async () => {
      const { data: { user }, error } = await supabase.auth.getUser();
      if (error) {
        console.error('Error fetching user:', error.message);
      }
      setUser(user);
      // Optionally fetch session too if needed
      // const { data: { session }, error: sessionError } = await supabase.auth.getSession();
      // if (sessionError) console.error('Error fetching session:', sessionError.message);
      // setSession(session);
      setLoading(false);
    };

    fetchUser();

    // Subscribe to authentication changes
    const { data: authListener } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setSession(session);
        setUser(session?.user || null);
        setLoading(false);
      }
    );

    return () => {
      // Cleanup the subscription
      // authListener.unsubscribe(); // This might be deprecated or handled differently depending on Supabase version
    };
  }, []);

  return { user, session, loading };
};

This hook listens for authentication state changes. When the user logs in or out, the onAuthStateChange listener fires, updating the user and session state. Now, to protect a route, say a dashboard page (pages/dashboard.js), you can use this hook:

import { useRouter } from 'next/router';
import { useUser } from '../hooks/useUser'; // Assuming you put the hook in src/hooks/useUser.js

export default function DashboardPage() {
  const router = useRouter();
  const { user, loading } = useUser();

  if (loading) {
    return <p>Loading...</p>;
  }

  if (!user) {
    // If no user, redirect to the login page
    router.push('/auth/login');
    return null; // Return null to prevent rendering anything
  }

  // If user is logged in, render the protected content
  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {user.email}!</p>
      {/* Protected content goes here */}
    </div>
  );
}

This is a client-side protection strategy. The page initially loads, checks for the user, and redirects if they aren't logged in. For more robust security, especially for sensitive data fetching, you might want to implement server-side checks using Next.js API routes or getServerSideProps with a server-side Supabase client (using the service role key). The onAuthStateChange is super powerful because it allows your UI to react in real-time to login/logout events without requiring a full page refresh. This is key for a smooth user experience. The getUser() function fetches the currently authenticated user, and if there's no active session, it returns null. The loading state is important to prevent flickering or attempting to redirect before the authentication status is determined. Once the loading state is false, we check if user exists. If not, we use useRouter to navigate the user to the login page. If a user does exist, we render the protected content. Remember, this is client-side. For critical operations, always validate on the server. The supabase.auth.onAuthStateChange function is a subscription. It returns a subscription object. When the component unmounts (as handled by the useEffect cleanup function), you should ideally unsubscribe to prevent memory leaks, although the Supabase client often handles this gracefully. The way onAuthStateChange works is by emitting events whenever the authentication state changes (e.g., user signs in, signs out, token refreshes). The second argument in the callback is the session object, which contains user information and authentication tokens. By updating our React state with this information, our component automatically re-renders to reflect the new authentication status. This reactive nature is what makes building dynamic UIs with Supabase so seamless. This approach effectively safeguards your sensitive pages from unauthorized access, ensuring only logged-in users can see the dashboard content. It's a fundamental part of building secure applications, and Supabase makes it surprisingly straightforward.

Handling Log Out

Finally, let's add the log out functionality. This is as simple as calling a method on the Supabase client. You'll typically want a button on your dashboard or profile page that, when clicked, signs the user out.

Here's how you can implement the logout handler:

import { supabase } from '../supabaseClient';
import { useRouter } from 'next/router';

const handleLogout = async () => {
  const { error } = await supabase.auth.signOut();
  if (error) {
    console.error('Logout error:', error.message);
  } else {
    // Redirect to the login page or homepage after logout
    useRouter().push('/auth/login');
  }
};

// In your component, you'd have a button like:
// <button onClick={handleLogout}>Logout</button>

When supabase.auth.signOut() is called, it invalidates the user's session on the server and clears the session cookie. The onAuthStateChange listener we set up earlier will automatically detect this session termination and update the user's state in our application to null. This means our protected routes will then correctly redirect unauthenticated users. It's a clean and efficient process. You'll notice I used useRouter() directly inside the handleLogout function. While this works for demonstration, it's generally better practice to call hooks like useRouter only at the top level of your React components. So, a more conventional way would be to get the router instance within the component and pass the handleLogout function as a prop or define it within the component where useRouter is available. For example, in your DashboardPage component:

// Inside DashboardPage component...
const router = useRouter();

const handleLogout = async () => {
  const { error } = await supabase.auth.signOut();
  if (error) {
    console.error('Logout error:', error.message);
  } else {
    router.push('/auth/login');
  }
};

return (
  <div>
    {/* ... other dashboard content ... */}
    <button onClick={handleLogout}>Logout</button>
  </div>
);

This ensures that React's hook rules are followed. Logging out is a critical security measure, ensuring that users can securely end their sessions. Supabase handles the backend invalidation, and our frontend reacts to it, providing a seamless transition back to an unauthenticated state. This completes the core functionalities of our Supabase Auth Next.js tutorial: sign-up, login, session management, protected routes, and log out. You've now got a solid foundation for building authenticated applications with Next.js and Supabase. Keep experimenting, keep building, and happy coding, guys!