Next.js 15 introduces stricter rules about where and how you can fetch data, especially when dealing with authentication and cookies. This guide explains the essential concepts and shows you exactly what to do.
Think of Next.js 15 as having two distinct environments for server code:
layout.tsx, page.tsx, and other components"use server"Server Components need to be predictable and cacheable. If they could modify cookies or redirect users, it would break React's rendering model and cause unpredictable behavior.
Error: Cookies can only be modified in a Server Action or Route Handler
When you use Supabase (or similar auth libraries) in a Server Component like this:
// ❌ This will break in layout.tsx or page.tsx
const supabase = await createClient();
const { data: user } = await supabase.auth.getUser(); // Tries to write cookies!
The getUser() method might refresh tokens and attempt to write new cookies, but Server Components can't do that.
For Server Components (reading data):
// utils/supabase/server-read.ts
export async function createReadOnlySupabase() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
// No setAll method = read-only
},
}
);
}
For Server Actions (changing data):
// utils/supabase/server-action.ts
export async function createActionSupabase() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
},
},
}
);
}
// app/dashboard/page.tsx
export default async function DashboardPage() {
const supabase = await createReadOnlySupabase();
const { data: user } = await supabase.auth.getUser();
if (!user) {
return <div>Please log in</div>;
}
return <div>Welcome, {user.email}!</div>;
}
// app/actions/auth.ts
"use server";
export async function logout() {
const supabase = await createActionSupabase();
await supabase.auth.signOut();
redirect("/login");
}
// app/components/LogoutButton.tsx
export function LogoutButton() {
return (
<form action={logout}>
<button type="submit">Sign Out</button>
</form>
);
}
In Next.js 15, you must await the cookies() function:
// ✅ Correct
const cookieStore = await cookies();
// ❌ Wrong (worked in Next.js 14)
const cookieStore = cookies();
The rules about cookie modification are now enforced at runtime:
| Operation | Server Components | Server Actions | Route Handlers |
|---|---|---|---|
| Read cookies | ✅ Yes | ✅ Yes | ✅ Yes |
| Write cookies | ❌ No | ✅ Yes | ✅ Yes |
| Delete cookies | ❌ No | ✅ Yes | ✅ Yes |
When building a page that shows data:
When handling user actions:
When you need full control:
app/api/)Next.js 15's strict separation keeps your app fast and predictable. Remember:
Follow these patterns, and you'll avoid the common errors that trip up many developers when upgrading to Next.js 15.