OWolf

AboutBlogProjects
©2025 OWolf.com

Privacy

Contact

Supabase Authentication With Next.Js 15 App Router

October 21, 2025

O. Wolfson

Supabase Authentication with Next.js 15 App Router

Setting up authentication in modern web applications can be complex, but with Supabase and Next.js 15's App Router, it becomes surprisingly straightforward. This guide will walk you through a complete authentication system with login, signup, logout, and protected routes.

Table of Contents

  1. Prerequisites
  2. Supabase Dashboard Setup
  3. Next.js Project Setup
  4. Environment Configuration
  5. Supabase Client Configuration
  6. Authentication Components
  7. Server Actions
  8. Middleware Setup
  9. Protected Routes
  10. Testing the Implementation
  11. Key Features
  12. Production Considerations
  13. Conclusion

Prerequisites

  • Node.js 18+ installed
  • A Supabase account (free tier available)
  • Basic knowledge of React and Next.js

Supabase Dashboard Setup

Step 1: Create a New Project

  1. Go to supabase.com → New Project

  2. Choose your organization

  3. Enter:

    • Name: your-app-name
    • Database Password: strong password
    • Region: closest to your users
  4. Click Create new project

Step 2: Configure Authentication Settings

  1. Authentication → Settings

  2. Set Site URL:

    • Dev: http://localhost:3000
    • Prod: https://yourdomain.com
  3. Redirect URLs:

    http://localhost:3000/auth/confirm
    http://localhost:3000/auth/error
    http://localhost:3000/dashboard
    http://localhost:3000/
    
  4. Enable Email provider

  5. Disable “Enable email confirmations” (for dev)

Step 3: Get API Keys

  • Settings → API

  • Copy:

    • Project URL
    • Anon key
    • Service role key (keep secret)

Next.js Project Setup

Step 1: Create Next.js Project

bash
npx create-next-app@latest my-auth-app
cd my-auth-app

Step 2: Install Supabase Dependencies

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

Environment Configuration

Create .env.local:

env
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key

Supabase Client Configuration

Client-Side (lib/supabase/client.ts)

typescript
"use client";
import { createBrowserClient } from "@supabase/ssr";

export function createClient() {
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
  const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
  return createBrowserClient(url, anonKey);
}

Server-Side (lib/supabase/server.ts)

typescript
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createServerClientWithCookies() {
  const cookieStore = await cookies();
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore
            .getAll()
            .map((c) => ({ name: c.name, value: c.value }));
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => {
            cookieStore.set(name, value, options as CookieOptions);
          });
        },
      },
    }
  );
  return supabase;
}

Admin Client (lib/supabase/admin.ts)

typescript
import { createClient } from "@supabase/supabase-js";

if (!process.env.SUPABASE_SERVICE_ROLE_KEY) {
  throw new Error("SUPABASE_SERVICE_ROLE_KEY is not set");
}

export const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
  {
    auth: { persistSession: false },
    global: { headers: { "X-Client-Info": "my-app" } },
  }
);

Authentication Components

Includes AuthButton, LogoutButton, and LoginPage with client/server separation for optimal SSR.

(Full code unchanged.)


Server Actions

typescript
"use server";
import { createServerClientWithCookies } from "@/lib/supabase/server";
import { redirect } from "next/navigation";

export async function logout() {
  const supabase = await createServerClientWithCookies();
  await supabase.auth.signOut();
  redirect("/");
}

Middleware Setup

The middleware is crucial for maintaining user sessions across page navigations. It automatically refreshes authentication tokens and ensures both the server and client have synchronized cookies.

Create middleware.ts in the root of your project:

typescript
import { createServerClient } 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: {
        getAll() {
          return request.cookies.getAll().map((cookie) => ({
            name: cookie.name,
            value: cookie.value,
          }));
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => {
            request.cookies.set(name, value);
            response.cookies.set(name, value, options);
          });
        },
      },
    }
  );

  // Refresh session if expired
  const {
    data: { user },
  } = await supabase..();

  
   (request...() && !user) {
     url = request..();
    url. = ;
     .(url);
  }

   response;
}

  config = {
  : [
    ,
  ],
};

Key Points:

  • Runs on every request before the page loads
  • Automatically refreshes expired sessions
  • Protects routes by redirecting unauthenticated users
  • Keeps server and client cookies in sync

Protected Routes

Protected routes ensure only authenticated users can access certain pages. Here's how to implement them using server-side rendering with Next.js 15.

Protected Dashboard Page

Create app/dashboard/page.tsx:

typescript
import { createServerClientWithCookies } from "@/lib/supabase/server";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
  const supabase = await createServerClientWithCookies();

  // Check if user is authenticated
  const {
    data: { user },
  } = await supabase.auth.getUser();

  // Redirect to login if not authenticated
  if (!user) {
    redirect("/auth/login");
  }

  // Render protected content
  return (
    <div className="container mx-auto p-8">
      <h1 className="text-3xl font-bold mb-4">Dashboard</h1>
      <div className="bg-white rounded-lg shadow p-6">
        <h2 className="text-xl font-semibold mb-2">Welcome, {user.email}!</h2>
        <p className="text-gray-600">
          This is a protected route. Only authenticated users can see this page.
        </p>
        <div className="mt-4 p-4 bg-gray-100 rounded">
          User ID: {user.id}
        
      
    
  );
}

Alternative: Using a Layout Protection

For protecting multiple routes at once, create a layout wrapper at app/protected/layout.tsx:

typescript
import { createServerClientWithCookies } from "@/lib/supabase/server";
import { redirect } from "next/navigation";

export default async function ProtectedLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const supabase = await createServerClientWithCookies();

  const {
    data: { user },
  } = await supabase.auth.getUser();

  if (!user) {
    redirect("/auth/login");
  }

  return <>{children}</>;
}

Then create protected routes under app/protected/:

typescript
// app/protected/settings/page.tsx
export default async function SettingsPage() {
  const supabase = await createServerClientWithCookies();
  const {
    data: { user },
  } = await supabase.auth.getUser();

  return (
    <div>
      <h1>Settings</h1>
      <p>Protected content for {user?.email}</p>
    </div>
  );
}

Benefits:

  • Server-side authentication check (no flash of protected content)
  • Automatic redirect to login
  • Works seamlessly with middleware
  • Type-safe with TypeScript

Testing the Implementation

Follow the step-by-step flow to test login, signup, logout, and cookie persistence.


Key Features of This Implementation

✅ Server-Side Rendering (SSR) ✅ Automatic Session Management ✅ Type Safety ✅ Modern Next.js Patterns


Production Considerations

  • Enable email confirmations
  • Secure environment variables
  • Never expose service role key
  • Add rate limiting and error boundaries

Conclusion

This setup provides a complete, production-ready authentication system using Supabase and Next.js 15. With SSR, secure cookies, and simple APIs, you can build robust user authentication without added complexity.


▊
auth
getUser
// Protect dashboard route
if
nextUrl
pathname
startsWith
"/dashboard"
const
nextUrl
clone
pathname
"/auth/login"
return
NextResponse
redirect
return
export
const
matcher
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)"
<p className="text-sm text-gray-500">
</p>
</div>
</div>
</div>