OWolf

BlogToolsProjectsAboutContact
© 2025 owolf.com
HomeAboutNotesContactPrivacy

2025-07-02 Web Development, Programming

Handling Authentication in Next.js App Router with Supabase: Server, Client, and Sync

By O. Wolfson

Authentication in Next.js (especially with the App Router and SSR) is fundamentally different from classic React single-page apps. If you’re using Supabase Auth with Next.js, understanding these differences is crucial for a reliable, responsive user experience.

Why Next.js Is Different

  • Runs code on both the server (SSR, server actions, API routes) and the client (browser UI).
  • Supabase’s JS client stores the session in the browser (localStorage), but SSR/server code only sees cookies or headers.

If your app logic only lives on the client, auth is simple. But with Next.js, your UI, API, and SSR-rendered pages all need to be aware of the user's auth state.


Sign In: Why It Must Go Through the Server

When a user signs in:

  • You must set auth cookies on the server, so both SSR code and the browser agree on the user session.
  • If you only use supabase.auth.signInWithPassword() on the client, your server (SSR, API, server actions) has no idea who the user is!

Solution: Handle login with a server action (or API route) that sets the Supabase auth cookie, then redirect the user to the dashboard or home.


Sign Out: Why Client Matters

When a user signs out:

  • The UI should update instantly.
  • The browser's Supabase session/localStorage should be cleared.
  • Ideally, the server-side cookies should also be cleared, so SSR and API routes no longer see an active session.

Solution:

  • Call supabase.auth.signOut() on the client for instant UI updates and session clearing.
  • (Optional but recommended) Also hit a server action or API route that clears the cookies, ensuring SSR matches the browser.

Keeping UI and Auth State in Sync

Client UI:

  • Use a client component (useEffect + Supabase’s onAuthStateChange) to subscribe to auth changes.
  • When the session changes (login or logout), update state and UI instantly.

Server:

  • Reads session only from cookies. If the cookie isn’t set (or isn’t cleared), SSR and API routes may show stale data!

Example: Typical Next.js Auth Flow

Sign In (Server Action):

js
// /app/actions/auth/sign-in-action.ts
"use server";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";

export async function signInAction(formData: FormData) {
  const supabase = createClient();
  // ...sign in with email/password
  // set auth cookie
  redirect("/"); // to trigger UI update/SSR
}

Sign Out (Client UI):

tsx
"use client";
import { useRouter } from "next/navigation";
import { createClient } from "@/utils/supabase/client";

function SignOutMenuItem() {
  const router = useRouter();
  const supabase = createClient();

  const handleSignOut = async () => {
    await supabase.auth.signOut(); // clears localStorage & notifies listeners
    router.refresh(); // updates SSR-aware UI
    router.push("/sign-in"); // go to sign in
  };

  return <button onClick={handleSignOut}>Log out</button>;
}

Navbar (Client):

tsx
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/utils/supabase/client";
// ...rest of your nav
useEffect(() => {
  // Get initial user
  // Subscribe to onAuthStateChange
  // Update state for UI when session changes
}, []);

Common Pitfalls

  • Logging in only on the client: SSR will NOT know about the user! Protected routes won’t work; navbar won’t update on SSR.
  • Logging out only on the server: Client UI may not update instantly. The browser still thinks you’re logged in until refresh.

Best Practices

  • Login: Use server actions or API routes for sign in, set cookies, redirect to a page.
  • Logout: Use client-side sign out for instant feedback and subscribe to state changes. (Optionally, also clear cookies server-side.)
  • SSR-Protected Routes: Always check cookies/server session.

Summary Table

ActionClassic SPANext.js (App Router)
Sign InClient onlyServer Action/API Route (set cookie)
Sign OutClient onlyClient (UI, clear session), (optionally server)
SSR SyncNot neededCritical: server must know about session

Further Reading

  • Supabase Next.js Auth Docs
  • Next.js App Router Docs

In summary:

  • Server handles login for SSR.
  • Client handles logout for instant UI.
  • Keep them in sync for a smooth user experience.

Absolutely! Here’s section a added to the end of the article—showing a copy-paste ready “universal” auth hook pattern for both SSR and client components in Next.js App Router + Supabase.


a. Copy-Paste: Universal Auth Pattern (SSR + Client)

Here’s how you can access the user in both server and client components, using Supabase, in a way that always gets the current session.

For Server Components/Server Actions

ts
// utils/supabase/server.ts
import { cookies } from "next/headers";
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";

export function createClient() {
  return createServerComponentClient({ cookies });
}

// Example in a server component
import { createClient } from "@/utils/supabase/server";
export default async function Page() {
  const supabase = createClient();
  const {
    data: { user },
  } = await supabase.auth.getUser();
  // ...rest
}

For Client Components

ts
// utils/supabase/client.ts
import { createBrowserClient } from "@supabase/auth-helpers-nextjs";

export function createClient() {
  return createBrowserClient();
}

// Example in a client component
import { useEffect, useState } from "react";
import { createClient } from "@/utils/supabase/client";

export function useSupabaseUser() {
  const [user, setUser] = useState(null);
  const supabase = createClient();

  useEffect(() => {
    supabase.auth.getUser().then(({ data }) => setUser(data.user));
    const { data: listener } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setUser(session?.user ?? null);
      }
    );
    return () => listener.subscription.unsubscribe();
  }, [supabase]);

  return user;
}

Usage Example

tsx
// In a client component (navbar, etc.)
import { useSupabaseUser } from "@/utils/supabase/client";

export function MyNavbar() {
  const user = useSupabaseUser();
  return <span>{user ? user.email : "Guest"}</span>;
}

Result:

  • Server code always uses cookies for session.
  • Client code always listens to state changes instantly.
  • UI always stays in sync, no matter how auth changes.