This article intends to save you 10+ hours of your valuable time, effort, and ‘developer’ pain, which I think is an entirely different kind of pain
In your Next.js project, you'll need a way to keep UI elements like your Navbar (a Client component) in sync with Supabase Auth state
You might be thinking - React Context to the rescue.
But wait!
Next.js Authentication Guide states:
React
context
is not supported in Server Components, making them only applicable to Client Components.
Your Navbar is a Client component, seems all good.
But,
Supabase Server-Side Auth recommends access to Auth User data, using only supabase.auth.getUser()
in Server Components.
Example from Supabase Official Guide → Setting up Server-Side Auth for Next.js
// app/private/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
export default async function PrivatePage() {
const supabase = await createClient()
const { data, error } = await supabase.auth.getUser()
if (error || !data?.user) {
redirect('/login')
}
return <p>Hello {data.user.email}</p>
}
This creates a disconnect between how you access Auth data on the server versus the client.
So, how can your Client components (like Navbar) & Server components be in sync with the authentication state?
Let's find out.
The Flow Works Like This:
- Root layout fetches the initial user state server-side
- This data initializes the AuthProvider
- Navbar and other client components access auth state via the context
- When auth changes (login/logout), the context updates all components
- Server Components independently verify auth status on each request
1. Server-Side Authentication Layer
First, create a reliable server-side authentication layer:
// utils/auth.ts
import { createClient } from '@/utils/supabase/server';
import { User } from '@supabase/supabase-js';
export async function getUser(): Promise<User | null> {
const supabase = await createClient();
const { data, error } = await supabase.auth.getUser();
if (error || !data?.user) {
return null;
}
return data.user;
}
2. Layout-Based Authentication Sharing
Use Next.js layouts to fetch Auth data from server side once and pass it down:
// app/layout.tsx
import { getUser } from '@/utils/auth';
import Navbar from '@/components/Navbar';
import { AuthProvider } from '@/components/AuthProvider';
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getUser();
return (
<html lang="en">
<body>
<AuthProvider initialUser={user}>
<Navbar />
{children}
</AuthProvider>
</body>
</html>
);
}
3. Client-Side Auth Provider
Create a React Context to watch for Auth change & provide Auth context to all Client components:
// components/AuthProvider.tsx
"use client";
import { createContext, useState, useEffect, useContext, ReactNode } from 'react';
import { User } from '@supabase/supabase-js';
import { createClient } from '@/utils/supabase/client';
type AuthContextType = {
user: User | null;
isLoading: boolean;
};
const AuthContext = createContext<AuthContextType>({
user: null,
isLoading: true,
});
export const useAuth = () => useContext(AuthContext);
export function AuthProvider({
children,
initialUser
}: {
children: ReactNode;
initialUser: User | null;
}) {
const [user, setUser] = useState<User | null>(initialUser);
const [isLoading, setIsLoading] = useState(false);
const supabase = createClient();
useEffect(() => {
// Initialize with SSR data
setUser(initialUser);
setIsLoading(false);
// Listen for auth changes on the client
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user || null);
}
);
return () => subscription.unsubscribe();
}, [initialUser]);
return (
<AuthContext.Provider value={{ user, isLoading }}>
{children}
</AuthContext.Provider>
);
}
4. Client-Side Auth Synchronization
For your client components (like Navbar), in addition to consuming Auth Context from AuthProvider, create a mechanism to sync with auth changes:
// components/Navbar.tsx
"use client";
import { useAuth } from '@/components/AuthProvider';
import { signOut } from "app/auth/actions";
import Link from 'next/link';
export default function Navbar() {
const { user } = useAuth();
const supabase = createClient();
const handleSignOut = async () => {
await supabase.auth.signOut();
};
return (
<nav className="p-4 flex justify-between items-center bg-gray-100">
<Link href="/" className="font-bold text-lg">My App</Link>
<div>
{user ? (
<div className="flex items-center gap-4">
<span>Hello, {user.email}</span>
<form action={signOut} className="w-full">
<button
type="submit"
className="w-full text-left"
>
Logout
</button>
</form>
</div>
) : (
<Link
href="/login"
className="px-4 py-2 bg-blue-500 text-white rounded"
>
Sign In
</Link>
)}
</div>
</nav>
);
}
5. Auth State for Server Components
For your server components, use the Supabase recommended pattern. For example:
// app/profile/page.tsx
import { redirect } from 'next/navigation';
import { getUser } from '@/utils/auth';
import ProfileDetails from '@/components/ProfileDetails';
export default async function ProfilePage() {
const user = await getUser();
if (!user) {
redirect('/login');
}
// Fetch additional user data from Supabase if needed
// This can include profile data beyond the auth user
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Profile</h1>
<ProfileDetails user={user} />
</div>
);
}
// Client Component that receives user data from parent
// components/ProfileDetails.tsx
"use client";
import { User } from '@supabase/supabase-js';
export default function ProfileDetails({ user }: { user: User }) {
return (
<div className="bg-white p-6 rounded shadow">
<h2 className="text-xl mb-4">Your Profile</h2>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>User ID:</strong> {user.id}</p>
<p><strong>Last Sign In:</strong> {new Date(user.last_sign_in_at || '').toLocaleString()}</p>
</div>
);
}
Benefits of This Approach
-
Server Components: Can access user data directly via
getUser()
-
Client Components: Get real-time auth state via the context hook (
useAuth()
) - No Duplication: Auth logic is centralized and consistent
- Performance: Server-side verification for protected routes
- Seamless UX: UI stays in sync with auth state
This approach gives you the best of both worlds - server-side protection for routes and data access while maintaining a reactive UI that responds to authentication changes