How I built a fullstack authentication system with Next.js, Supabase, and Tailwind
Email sign-in, session handling, and protected routes in a Next.js App Router app, built on Supabase auth with a clean Tailwind interface.
On this page
Authentication is the first real fullstack feature most projects need. Here is the shape that has served me well: Supabase for identity, Next.js for routing, and a thin server boundary.
Create the client once
Create a single browser client and reuse it. Creating a new one per render causes subtle session bugs.
import { createClient } from "@supabase/supabase-js";
let client: ReturnType<typeof createClient> | null = null;
export function getSupabase() {
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!url || !key) return null;
client ??= createClient(url, key);
return client;
}
Returning null when the environment is missing means the UI can degrade gracefully instead of crashing.
Keep secrets on the server
The anon key is public by design, protected by row level security. Service keys are not.
- Never import a service key into a client component
- Enforce access with row level security, not with UI logic
- Treat the client as untrusted at all times
Protect routes at the edge
A redirect in a layout is the simplest reliable guard.
export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const session = await getSession();
if (!session) redirect("/login");
return <>{children}</>;
}
The form should feel instant
Authentication UX lives or dies on feedback. Disable the button, show a pending state, and announce the result.
<button disabled={isPending} aria-busy={isPending}>
{isPending ? "Signing in..." : "Sign in"}
</button>
Never tell the user whether an email exists. Generic errors protect your users from enumeration.
Checklist
- One client, created lazily
- Secrets only on the server
- Row level security as the real boundary
- Route guards in layouts
- Optimistic, accessible feedback on every action
Continue reading
Related articles
Serverless file uploads with Supabase Storage and Next.js API routes
Secure, serverless file uploads in Next.js using Supabase Storage, with validation, size limits, and public access handling.
Building a modern todo app with Zustand, Firebase, and Next.js
A real-time todo app with authentication, CRUD operations, and predictable global state, built with Next.js, Firebase, and Zustand.
Integrating Razorpay payments in Next.js: a step-by-step guide
Set up checkout, verify signatures on the server, and handle webhooks safely when integrating Razorpay with Next.js.