Join our new Affiliate Program!
    Implementing Supabase Auth in Next.js: The Complete SSR Guide

    authentication

    nextjs

    supabase

    Implementing Supabase Auth in Next.js: The Complete SSR Guide

    The New Approach to Supabase Auth

    With the release of the @supabase/ssr package, implementing authentication in Next.js has become significantly more streamlined. This package replaces the previous auth-helpers libraries and works across multiple frameworks, providing a consistent approach regardless of which framework you're using.

    In this guide, we'll implement Supabase Auth in a Next.js application using the new SSR package.

    Understanding the Supabase SSR Package

    The @supabase/ssr package was developed to address the evolving landscape of modern frameworks. As frameworks like Next.js increasingly lean toward server-side rendering, Supabase needed a solution that worked seamlessly in both server and client environments.

    Key benefits of the SSR package include:

    1. Framework-agnostic design
    2. Simplified cookie management
    3. Consistent auth state across server and client
    4. Improved type safety with TypeScript

    Setup: Installing the Required Packages

    Let's start by installing the necessary packages:

    npm install @supabase/supabase-js @supabase/ssr

    Creating the Supabase Client

    Next, let's create a shared module for our Supabase client. Create a file called lib/supabase.ts:

    import { createServerClient, type CookieOptions } from "@supabase/ssr";
    import { createClient } from "@supabase/supabase-js";
    import { cookies } from "next/headers";
     
    export const createServerSupabaseClient = () => {
      const cookieStore = cookies();
     
      return createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
          cookies: {
            get(name: string) {
              return cookieStore.get(name)?.value;
            },
            set(name: string, value: string, options: CookieOptions) {
              cookieStore.set({ name, value, ...options });
            },
            remove(name: string, options: CookieOptions) {
              cookieStore.set({ name, value: "", ...options });
            },
          },
        }
      );
    };
     
    export const createClientSupabaseClient = () => {
      return createClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
          auth: {
            persistSession: true,
          },
        }
      );
    };

    Server-Side Authentication with Route Handlers

    Let's implement the core authentication endpoints using Next.js route handlers:

    Sign In Endpoint

    Create a file at app/api/auth/sign-in/route.ts:

    import { createServerSupabaseClient } from "@/lib/supabase";
    import { redirect } from "next/navigation";
    import { NextResponse } from "next/server";
     
    export async function POST(request: Request) {
      const requestUrl = new URL(request.url);
      const formData = await request.formData();
      const email = formData.get("email") as string;
      const password = formData.get("password") as string;
      const redirectTo = requestUrl.searchParams.get("redirect_to") || "/";
     
      const supabase = createServerSupabaseClient();
     
      const { error } = await supabase.auth.signInWithPassword({
        email,
        password,
      });
     
      if (error) {
        return NextResponse.json({ error: error.message }, { status: 400 });
      }
     
      return NextResponse.redirect(new URL(redirectTo, requestUrl.origin), {
        status: 301,
      });
    }

    Sign Up Endpoint

    Create a file at app/api/auth/sign-up/route.ts:

    import { createServerSupabaseClient } from "@/lib/supabase";
    import { NextResponse } from "next/server";
     
    export async function POST(request: Request) {
      const requestUrl = new URL(request.url);
      const formData = await request.formData();
      const email = formData.get("email") as string;
      const password = formData.get("password") as string;
      const redirectTo = requestUrl.searchParams.get("redirect_to") || "/";
     
      const supabase = createServerSupabaseClient();
     
      const { error } = await supabase.auth.signUp({
        email,
        password,
        options: {
          emailRedirectTo: `${requestUrl.origin}/api/auth/callback`,
        },
      });
     
      if (error) {
        return NextResponse.json({ error: error.message }, { status: 400 });
      }
     
      return NextResponse.redirect(new URL(redirectTo, requestUrl.origin), {
        status: 301,
      });
    }

    Callback Endpoint

    Create a file at app/api/auth/callback/route.ts:

    import { createServerSupabaseClient } from "@/lib/supabase";
    import { NextResponse } from "next/server";
     
    export async function GET(request: Request) {
      const requestUrl = new URL(request.url);
      const code = requestUrl.searchParams.get("code");
     
      if (code) {
        const supabase = createServerSupabaseClient();
        await supabase.auth.exchangeCodeForSession(code);
      }
     
      return NextResponse.redirect(new URL("/", requestUrl.origin));
    }

    Sign Out Endpoint

    Create a file at app/api/auth/sign-out/route.ts:

    import { createServerSupabaseClient } from "@/lib/supabase";
    import { NextResponse } from "next/server";
     
    export async function POST(request: Request) {
      const requestUrl = new URL(request.url);
      const supabase = createServerSupabaseClient();
     
      await supabase.auth.signOut();
     
      return NextResponse.redirect(new URL("/login", requestUrl.origin), {
        status: 301,
      });
    }

    Client-Side Authentication Components

    Now let's create the UI components that interact with these endpoints:

    Login Form Component

    Create a file at components/auth/LoginForm.tsx:

    "use client";
     
    import { useState } from "react";
    import { useRouter } from "next/navigation";
     
    export default function LoginForm() {
      const [email, setEmail] = useState("");
      const [password, setPassword] = useState("");
      const [error, setError] = useState<string | null>(null);
      const [isLoading, setIsLoading] = useState(false);
     
      const router = useRouter();
     
      const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault();
        setIsLoading(true);
        setError(null);
     
        try {
          const formData = new FormData();
          formData.append("email", email);
          formData.append("password", password);
     
          const response = await fetch("/api/auth/sign-in", {
            method: "POST",
            body: formData,
          });
     
          if (!response.ok) {
            const data = await response.json();
            throw new Error(data.error || "Failed to sign in");
          }
     
          router.refresh();
        } catch (err) {
          setError(err instanceof Error ? err.message : "An error occurred");
        } finally {
          setIsLoading(false);
        }
      };
     
      return (
        <form onSubmit={handleSubmit} className="space-y-4">
          {error && (
            <div className="p-3 bg-red-100 text-red-700 rounded">{error}</div>
          )}
     
          <div>
            <label htmlFor="email" className="block text-sm font-medium">
              Email
            </label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              className="mt-1 block w-full rounded border border-gray-300 px-3 py-2"
            />
          </div>
     
          <div>
            <label htmlFor="password" className="block text-sm font-medium">
              Password
            </label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              className="mt-1 block w-full rounded border border-gray-300 px-3 py-2"
            />
          </div>
     
          <button
            type="submit"
            disabled={isLoading}
            className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 disabled:opacity-50"
          >
            {isLoading ? "Signing in..." : "Sign in"}
          </button>
        </form>
      );
    }

    Protecting Routes with Server Components

    One of the biggest advantages of the SSR package is the ability to protect routes using server components. Let's create a middleware function to check authentication:

    Create a file at middleware.ts:

    import { createServerClient, type CookieOptions } from "@supabase/ssr";
    import { NextResponse, type NextRequest } from "next/server";
     
    export async function middleware(request: NextRequest) {
      let response = NextResponse.next({
        request: {
          headers: request.headers,
        },
      });
     
      const supabase = createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
          cookies: {
            get(name: string) {
              return request.cookies.get(name)?.value;
            },
            set(name: string, value: string, options: CookieOptions) {
              response.cookies.set({ name, value, ...options });
            },
            remove(name: string, options: CookieOptions) {
              response.cookies.set({ name, value: "", ...options });
            },
          },
        }
      );
     
      const {
        data: { session },
      } = await supabase.auth.getSession();
     
      // If the user is not signed in and the route is protected, redirect to login
      const isProtectedRoute = request.nextUrl.pathname.startsWith("/dashboard");
      if (!session && isProtectedRoute) {
        return NextResponse.redirect(new URL("/login", request.url));
      }
     
      // If the user is signed in and trying to access login/signup, redirect to dashboard
      const isAuthRoute =
        request.nextUrl.pathname.startsWith("/login") ||
        request.nextUrl.pathname.startsWith("/signup");
      if (session && isAuthRoute) {
        return NextResponse.redirect(new URL("/dashboard", request.url));
      }
     
      return response;
    }
     
    // Specify which routes the middleware should run on
    export const config = {
      matcher: ["/dashboard/:path*", "/login", "/signup"],
    };

    Creating a Session Provider for Client Components

    To access the authentication state in client components across your app, create a provider:

    Create a file at components/auth/SessionProvider.tsx:

    "use client";
     
    import { createContext, useContext, useEffect, useState } from "react";
    import { createClientSupabaseClient } from "@/lib/supabase";
    import type { Session, User } from "@supabase/supabase-js";
     
    type AuthContextType = {
      session: Session | null;
      user: User | null;
      isLoading: boolean;
      signOut: () => Promise<void>;
    };
     
    const AuthContext = createContext<AuthContextType>({
      session: null,
      user: null,
      isLoading: true,
      signOut: async () => {},
    });
     
    export const useAuth = () => useContext(AuthContext);
     
    export default function SessionProvider({
      children,
      initialSession,
    }: {
      children: React.ReactNode;
      initialSession: Session | null;
    }) {
      const [session, setSession] = useState<Session | null>(initialSession);
      const [user, setUser] = useState<User | null>(initialSession?.user || null);
      const [isLoading, setIsLoading] = useState(false);
     
      const supabase = createClientSupabaseClient();
     
      useEffect(() => {
        const {
          data: { subscription },
        } = supabase.auth.onAuthStateChange((_event, session) => {
          setSession(session);
          setUser(session?.user || null);
        });
     
        return () => {
          subscription.unsubscribe();
        };
      }, [supabase]);
     
      const signOut = async () => {
        setIsLoading(true);
        try {
          await fetch("/api/auth/sign-out", {
            method: "POST",
          });
        } finally {
          setIsLoading(false);
        }
      };
     
      return (
        <AuthContext.Provider value={{ session, user, isLoading, signOut }}>
          {children}
        </AuthContext.Provider>
      );
    }

    Initializing the Provider in Your Layout

    Finally, update your root layout to include the Session Provider:

    Modify your app/layout.tsx:

    import { createServerSupabaseClient } from "@/lib/supabase";
    import SessionProvider from "@/components/auth/SessionProvider";
     
    export default async function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      const supabase = createServerSupabaseClient();
      const {
        data: { session },
      } = await supabase.auth.getSession();
     
      return (
        <html lang="en">
          <body>
            <SessionProvider initialSession={session}>{children}</SessionProvider>
          </body>
        </html>
      );
    }

    Creating a Protected Dashboard Page

    Let's test our authentication by creating a protected dashboard page:

    Create a file at app/dashboard/page.tsx:

    import { createServerSupabaseClient } from "@/lib/supabase";
    import { redirect } from "next/navigation";
     
    export default async function DashboardPage() {
      const supabase = createServerSupabaseClient();
      const {
        data: { session },
      } = await supabase.auth.getSession();
     
      if (!session) {
        redirect("/login");
      }
     
      // Fetch user-specific data
      const { data: todos } = await supabase
        .from("todos")
        .select("*")
        .eq("user_id", session.user.id)
        .order("created_at", { ascending: false });
     
      return (
        <div className="max-w-4xl mx-auto p-4">
          <h1 className="text-2xl font-bold mb-6">Dashboard</h1>
     
          <div className="bg-white shadow rounded-lg p-6">
            <h2 className="text-xl font-semibold mb-4">
              Welcome, {session.user.email}
            </h2>
     
            <div className="mt-6">
              <h3 className="text-lg font-medium mb-3">Your Todos</h3>
              {todos && todos.length > 0 ? (
                <ul className="space-y-2">
                  {todos.map((todo) => (
                    <li key={todo.id} className="p-3 bg-gray-50 rounded">
                      {todo.title}
                    </li>
                  ))}
                </ul>
              ) : (
                <p className="text-gray-500">No todos yet.</p>
              )}
            </div>
     
            <form action="/api/auth/sign-out" method="post" className="mt-8">
              <button
                type="submit"
                className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
              >
                Sign Out
              </button>
            </form>
          </div>
        </div>
      );
    }

    Using the Auth State in Client Components

    Here's how to use the auth state in any client component:

    "use client";
     
    import { useAuth } from "@/components/auth/SessionProvider";
     
    export default function UserProfile() {
      const { user, signOut, isLoading } = useAuth();
     
      if (!user) {
        return <div>Not logged in</div>;
      }
     
      return (
        <div>
          <p>Logged in as: {user.email}</p>
          <button
            onClick={signOut}
            disabled={isLoading}
            className="bg-red-500 text-white px-4 py-2 rounded"
          >
            {isLoading ? "Signing out..." : "Sign Out"}
          </button>
        </div>
      );
    }

    Best Practices for Supabase Auth in Next.js

    Based on our implementation experience, here are some best practices:

    1. Keep cookie management consistent - Let Supabase handle cookies via the SSR package
    2. Use server components for data fetching - They provide better performance and security
    3. Implement RLS policies - Always secure your database with Row Level Security
    4. Refresh tokens automatically - The SSR package handles this for you
    5. Implement proper error handling - Always display meaningful error messages to users

    Conclusion

    The new @supabase/ssr package significantly simplifies authentication implementation in Next.js. By properly integrating server and client components, you can create a seamless authentication experience while maintaining the performance benefits of server-side rendering.

    This approach works particularly well with newer Next.js features like the App Router, Server Components, and Route Handlers, giving you a modern, type-safe authentication system for your Next.js application.

    Fekri

    Fekri

    Related Blogs

    Best SaaS Boilerplates to Build Your App in 2025

    development

    saas

    boilerplates

    Best SaaS Boilerplates to Build Your App in 2025

    A comprehensive comparison of the top SaaS boilerplates to accelerate your development process and get your product to market faster.

    Fekri

    Fekri

    April 01, 2025

    SaaS Payment Providers: The Ultimate Comparison Guide

    payments

    saas

    SaaS Payment Providers: The Ultimate Comparison Guide

    A concise comparison of Stripe, Lemon Squeezy, Polar, and Creem for SaaS businesses looking for the ideal payment solution.

    Fekri

    Fekri

    April 03, 2025

    The best SaaS Boilerplates to build your SaaS in 2024

    development

    saas

    boilerplates

    The best SaaS Boilerplates to build your SaaS in 2024

    Your Ultimate Guide to Speedy SaaS Development

    Fekri

    Fekri

    August 05, 2024

    Build
    faster using AI templates.

    AnotherWrapper gives you the foundation to build and ship fast. No more reinventing the wheel.

    Fekri — Solopreneur building AI startups
    Founder's Note

    Hi, I'm Fekri 👋

    @fekdaoui

    Over the last 15 months, I've built around 10 different AI apps. I noticed I was wasting a lot of time on repetitive tasks like:

    • Setting up tricky APIs
    • Generating vector embeddings
    • Integrating different AI models into a flow
    • Handling user input and output
    • Authentication, paywalls, emails, ...

    So I built something to make it easy.

    Now I can build a new AI app in just a couple of hours, leveraging one of the 10+ different AI demo apps.

    10+ ready-to-use apps

    10+ AI app templates to kickstart development

    Complete codebase

    Auth, payments, APIs — all integrated

    AI-ready infrastructure

    Vector embeddings, model switching, RAG

    Production-ready

    Secure deployment, rate limiting, error handling

    Get AnotherWrapper

    One-time purchase, lifetime access

    $249

    Pay once, use forever

    FAQ
    Frequently asked questions

    Have questions before getting started? Here are answers to common questions about AnotherWrapper.

    Still have questions? Email us at [email protected]