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’sonAuthStateChange
) 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
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 |
Further Reading
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.