July 2, 2025
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.
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.
When a user signs in:
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.
When a user signs out:
Solution:
supabase.auth.signOut() on the client for instant UI updates and session clearing.useEffect + Supabase’s onAuthStateChange) to subscribe to auth changes.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
}
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>;
}
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
}, []);
| Action | Classic SPA | Next.js (App Router) |
|---|---|---|
| Sign In | Client only | Server Action/API Route (set cookie) |
| Sign Out | Client only | Client (UI, clear session), (optionally server) |
| SSR Sync | Not needed | Critical: server must know about session |
In summary:
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.
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.
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
}
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;
}
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: