Part 2. Authentication
Goal: Master the complete registration - login - session management - route protection flow in Next.js + Supabase.
2.1 🎯 Learning Objectives
After completing this section, developers can:
- Understand how Supabase Auth works (JWT, claims, anon/service role key).
- Configure Next.js + Supabase for login/logout.
- Create basic login / signup / reset password UI.
- Protect routes with middleware (server-side).
- Attach user profile and metadata to database.
2.2 🔍 Supabase Auth Overview
Auth Architecture
Supabase Auth is based on:
- Postgres as core (user data stored in
auth.userstable) - JWT to authenticate client requests
- Policies (RLS) control access at DB layer
sequenceDiagram
Frontend->>Supabase Auth: Email/password, OAuth, Magic Link
Supabase Auth->>Postgres: Create user record
Postgres-->>Supabase Auth: Return user_id
Supabase Auth-->>Frontend: JWT (contains user_id, role, claims)
Frontend->>Database: Send query + JWT in header
Database->>Policy: Check `auth.uid()` → Allow / DenyKey Concepts
| Concept | Explanation |
|---|---|
| Anon Key | Public key used in frontend to call Supabase API (limited permissions). |
| Service Role Key | Used for backend (Edge Functions / server) - has permission to bypass RLS. |
| JWT Token | Contains user info (id, role, email, metadata). Sent in every request. |
| RLS (Row-Level Security) | Policy in DB checks auth.uid() to determine access permissions. |
2.3 🧱 Setup Auth in Next.js
Install Additional Packages
pnpm add @supabase/ssr@supabase/ssr helps maintain Supabase session on server (important for App Router).
Standard Internal Helper Structure
/lib/supabaseClient.ts
Update createClient to use createBrowserClient from @supabase/ssr.
import { createBrowserClient } from "@supabase/ssr";
export const createClient = () =>
createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);/lib/supabaseServer.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createServer() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
);
}Middleware (proxy) to Protect Routes
/proxy.ts
import { NextResponse, type NextRequest } from "next/server";
import { createServer } from "@/lib/supabaseServer";
export async function proxy(req: NextRequest) {
const res = NextResponse.next();
const supabase = await createServer();
const { data } = await supabase.auth.getSession();
// If no session → redirect to login page
if (!data.session && req.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/signin", req.url));
}
return res;
}
export const config = {
matcher: ["/dashboard/:path*"],
};Note: Middleware is deprecated and has been renamed to proxy.
2.4 🧭 Login / Signup / Logout Flow
Sign Up
/app/signup/page.tsx
"use client";
import { useState } from "react";
import { createClient } from "@/lib/supabaseClient";
export default function Signup() {
const supabase = createClient();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const handleSignup = async () => {
const { error } = await supabase.auth.signUp({ email, password });
if (error) alert(error.message);
else alert("Check your email to confirm signup!");
};
return (
<div>
<h2>Sign up</h2>
<input onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<input
type="password"
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button onClick={handleSignup}>Register</button>
</div>
);
}Sign In
/app/signin/page.tsx
"use client";
import { useState } from "react";
import { createClient } from "@/lib/supabaseClient";
import { useRouter } from "next/navigation";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const router = useRouter();
const supabase = createClient();
const handleLogin = async (email: string, password: string) => {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) alert(error.message);
else router.push("/dashboard");
};
return (
<div>
<h2>Sign in</h2>
<input onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<input
type="password"
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button onClick={() => handleLogin(email, password)}>Login</button>
</div>
);
}Sign Out
/app/components/LogoutButton.tsx
"use client";
import { createClient } from "@/lib/supabaseClient";
import { useRouter } from "next/navigation";
export default function LogoutButton() {
const router = useRouter();
const supabase = createClient();
const handleLogout = async () => {
await supabase.auth.signOut();
router.push("/signin");
};
return <button onClick={handleLogout}>Logout</button>;
}2.5 🧩 Sync User Profile
After signup, Supabase only has a record in auth.users. You should also sync a public.profiles table to store metadata or additional information.
SQL Migration
/supabase/migrations/20251105120000_create_profiles_table.sql
create table profiles (
id uuid references auth.users on delete cascade primary key,
full_name text,
avatar_url text,
created_at timestamptz default now()
);
alter table profiles enable row level security;
create policy "Users can view their own profile"
on profiles for select
using ((select auth.uid()) = id );
create policy "Users can insert their own profile"
on profiles for insert
with check ((select auth.uid()) = id );
create policy "Users can update their own profile"
on profiles
for update
using ((select auth.uid()) = id )
with check ((select auth.uid()) = id );Trigger to Auto-Create Profile
/supabase/migrations/20251105120100_trigger_sync_profile.sql
create function public.handle_new_user()
returns trigger
SET search_path = ''
as $$
begin
insert into public.profiles (id) values (new.id);
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();✅ When user signs up → trigger automatically creates corresponding profile.
2.6 🧩 Metadata & Claims
You can add custom metadata to JWT to support complex RLS (like role or org_id).
ℹ️ For a deeper dive into using JWT metadata with RLS, see Part 3 - Authorization (RLS & Policy).
Update Metadata
await supabase.auth.updateUser({
data: { role: "admin", organization_id: "org_abc123" },
});Access Metadata in Policy
create policy "Org members only"
on tasks
for select
using (
auth.jwt()->>'organization_id' = organization_id
);2.7 🛡️ Protect Routes (Protected Routes)
- Use middleware.ts (server-side) to redirect if not logged in.
- Use server component to fetch user session.
Example:
import { createServer } from "@/lib/supabaseServer";
export default async function DashboardPage() {
const supabase = createServer();
const { data } = await supabase.auth.getUser();
if (!data.user) return <div>Unauthorized</div>;
return <div>Welcome, {data.user.email}</div>;
}2.8 🧭 Password Reset & Magic Link
Send Reset Link
await supabase.auth.resetPasswordForEmail(email, {
redirectTo: "http://localhost:3000/reset-password",
});Magic Link Login (email-only)
await supabase.auth.signInWithOtp({
email: "user@example.com",
});When user clicks link in email → Supabase will automatically create session and redirect to FE.
2.9 ✅ Completion Checklist
- Understand Supabase Auth mechanism and JWT
- Signup / login / logout works
- Middleware redirects unauthenticated users
- Have
profilestable synced with user metadata - Understand how to add custom claims to JWT
- Can protect
/dashboardpage
2.10 💡 Internal Best Practices
- Never store passwords manually - always use
auth.signUpAPI. - Don't share service key with FE (only use anon key).
- Always sync user profile using trigger, don't create manually.
- Always enable RLS for every table related to users.
- Middleware only redirects - don't handle heavy logic.
- Clearly separate client/server Supabase client (
createClientvscreateServer). - Always test logout & expired session when implementing auth.
2.11 📚 References
2.12 🧾 Output After This Section
After completing Part 2, new developers must:
- Understand Auth architecture (JWT, anon key, service role).
- Create complete login/logout flow.
- Have middleware protecting
/dashboardroute. - Have
profilestable synced withauth.users. - Can add metadata (role, org_id) for use in RLS.