|$ curl https://forge-ai.dev/api/markdown?path=docs/nextjs/authentication
$cat docs/authentication-&-authorization-in-next.js.md
updated Last weekยท35 min readยทpublished

Authentication & Authorization in Next.js

โ—†Next.jsโ—†Authโ—†Securityโ—†Intermediate to Advanced๐ŸŽฏFree Tools
Introduction

Authentication and authorization are the gatekeepers of any real-world application. In Next.js, the decision of where to run auth logic โ€” inside a Server Component, in middleware, or on the client โ€” directly affects security, performance, and user experience. With the App Router and React Server Components, the safest place to check identity is on the server, where secrets never reach the browser and sessions can be verified before rendering starts.

Next.js does not ship with a built-in auth system. Instead, you compose one from primitives: the request/response objects in route handlers, the server-side cookies available in Server Components, middleware for lightweight route guards, and Server Actions for form submissions. Third-party libraries such as Auth.js and Clerk provide the actual identity plumbing, while Next.js provides the runtime and routing layer that wires them together.

This guide covers Auth.js v5 with the Next.js App Router, Clerk integration, route protection with middleware and role-based access, securing Server Actions, and choosing between JWT and database session strategies. The goal is a production-grade mental model: keep secrets on the server, verify every request, and minimize the attack surface of your application.

Authentication vs Authorization

Authenticationis the process of proving who you are. It answers the question: "Is this user really who they claim to be?" Common mechanisms include passwords, OAuth providers, magic links, and WebAuthn. The output of authentication is usually a session or token that the application can verify on subsequent requests.

Authorizationhappens after authentication. It answers: "What is this user allowed to do?" Authorization is where you enforce roles, permissions, ownership, and quotas. A user may be authenticated but not authorized to view an admin dashboard. Confusing the two is a common source of security bugs: proving identity does not grant permission.

ConcernAuthenticationAuthorization
Question answeredWho is this user?What can this user do?
Typical outputSession, JWT, or identity assertionRole, permission, or policy decision
Where it runsLogin provider, identity serverApplication code, middleware, database
ExamplesGoogle OAuth, credentials sign-inAdmin role, owner-only edit
Failure modeWrong identity, stolen credentialsPrivilege escalation, unauthorized access
๐Ÿ“

note

Always authenticate first, then authorize. Never mix the two decisions into a single function without clear separation. In Next.js, keep both checks server-side whenever possible.
Auth.js (NextAuth) v5 Setup

Auth.js v5, still often referred to as NextAuth, is the open-source authentication library for the web. It supports credentials, OAuth, email magic links, and passkeys. For Next.js 16 and the App Router, you install the next-auth package, create an auth.ts configuration file, and expose a catch-all route handler under /app/api/auth/[...nextauth]/route.ts.

Start with environment variables. Auth.js requires a AUTH_SECRET for token signing and session encryption. You can generate one with npx auth secret. OAuth providers need their client ID and client secret, and you should configure their callback URLs to point to /api/auth/callback/<provider>.

.env.local
Bash
1# Install Auth.js v5 for Next.js
2npm install next-auth@beta
3
4# Generate a strong AUTH_SECRET
5npx auth secret
6
7# .env.local
8AUTH_SECRET="your-generated-secret-min-32-chars"
9AUTH_GOOGLE_ID="your-google-client-id"
10AUTH_GOOGLE_SECRET="your-google-client-secret"
11AUTH_GITHUB_ID="your-github-client-id"
12AUTH_GITHUB_SECRET="your-github-client-secret"

The central configuration lives in auth.ts. This file defines providers, session strategy, callbacks, and database adapter. In the App Router, you import the auth helper from this file to read the session in Server Components, and use SessionProvider in a client wrapper when you need session state on the client.

auth.ts
TSX
1// auth.ts
2import NextAuth from "next-auth";
3import Credentials from "next-auth/providers/credentials";
4import Google from "next-auth/providers/google";
5import GitHub from "next-auth/providers/github";
6import { ZodError } from "zod";
7import { signInSchema } from "./lib/validation";
8
9export const { handlers, auth, signIn, signOut } = NextAuth({
10 providers: [
11 GitHub,
12 Google,
13 Credentials({
14 credentials: {
15 email: { label: "Email", type: "email" },
16 password: { label: "Password", type: "password" },
17 },
18 authorize: async (credentials) => {
19 try {
20 const { email, password } = await signInSchema.parseAsync(credentials);
21
22 // Replace with your real user lookup and password verification
23 const user = await getUserFromDb(email);
24 if (!user) return null;
25
26 const valid = await verifyPassword(password, user.passwordHash);
27 if (!valid) return null;
28
29 return {
30 id: user.id,
31 name: user.name,
32 email: user.email,
33 role: user.role,
34 };
35 } catch (error) {
36 if (error instanceof ZodError) return null;
37 throw error;
38 }
39 },
40 }),
41 ],
42 session: {
43 strategy: "jwt",
44 maxAge: 30 * 24 * 60 * 60, // 30 days
45 },
46 pages: {
47 signIn: "/login",
48 error: "/login",
49 },
50 callbacks: {
51 jwt({ token, user, trigger, session }) {
52 if (user) {
53 token.id = user.id;
54 token.role = user.role;
55 }
56 if (trigger === "update" && session?.name) {
57 token.name = session.name;
58 }
59 return token;
60 },
61 session({ session, token }) {
62 session.user.id = token.id as string;
63 session.user.role = token.role as string;
64 return session;
65 },
66 authorized({ auth, request: { nextUrl } }) {
67 const isLoggedIn = !!auth?.user;
68 const isProtected = nextUrl.pathname.startsWith("/dashboard");
69 if (isProtected && !isLoggedIn) {
70 const redirectUrl = new URL("/login", nextUrl);
71 redirectUrl.searchParams.set("callbackUrl", nextUrl.pathname);
72 return Response.redirect(redirectUrl);
73 }
74 return true;
75 },
76 },
77});

Expose the Auth.js HTTP API through a catch-all route handler. This single handler accepts sign-in, sign-out, callback, and session requests. The GET and POST methods are both delegated to the handlers exported from auth.ts.

route.ts
TSX
1// app/api/auth/[...nextauth]/route.ts
2import { handlers } from "@/auth";
3
4export const { GET, POST } = handlers;

Server Components can read the session directly with the auth helper. There is no need to wrap the app in a provider, which keeps the root layout server-rendered and avoids sending session data to the client unnecessarily.

page.tsx
TSX
1// app/page.tsx
2import { auth } from "@/auth";
3import { redirect } from "next/navigation";
4
5export default async function HomePage() {
6 const session = await auth();
7
8 if (!session) {
9 redirect("/login");
10 }
11
12 return (
13 <main>
14 <h1>Welcome, {session.user.name}</h1>
15 <p>Role: {session.user.role}</p>
16 </main>
17 );
18}

If you need session data on the client, for example to conditionally show UI after hydration, create a client component with the SessionProvider and use the useSession hook. Keep the provider as low in the tree as possible so that only the components that actually need it are wrapped.

client_session.tsx
TSX
1// components/AuthProvider.tsx
2"use client";
3
4import { SessionProvider } from "next-auth/react";
5import { ReactNode } from "react";
6
7export default function AuthProvider({ children }: { children: ReactNode }) {
8 return <SessionProvider>{children}</SessionProvider>;
9}
10
11// app/layout.tsx
12import AuthProvider from "@/components/AuthProvider";
13
14export default function RootLayout({ children }: { children: React.ReactNode }) {
15 return (
16 <html lang="en">
17 <body>
18 <AuthProvider>{children}</AuthProvider>
19 </body>
20 </html>
21 );
22}
23
24// components/UserStatus.tsx
25"use client";
26
27import { useSession, signIn, signOut } from "next-auth/react";
28
29export default function UserStatus() {
30 const { data: session, status } = useSession();
31
32 if (status === "loading") return <p>Loading...</p>;
33 if (!session) return <button onClick={() => signIn()}>Sign in</button>;
34
35 return (
36 <button onClick={() => signOut()}>
37 Sign out {session.user?.email}
38 </button>
39 );
40}
โ„น

info

Prefer the auth() helper in Server Components over useSession. The server-side check is faster, more secure, and avoids a client-side hydration boundary.
Clerk Integration

Clerk is a managed authentication platform that provides user management, organizations, roles, and UI components out of the box. It is a strong alternative to Auth.js when you want dashboards, user impersonation, and multi-organization support without building the backend infrastructure yourself.

Install the Clerk SDK, add your keys to .env.local, and wrap the root layout with ClerkProvider. Clerk uses its own middleware to manage session state, so you will also need a middleware.ts file at the project root.

.env.local
Bash
1# Install Clerk for Next.js
2npm install @clerk/nextjs
3
4# .env.local
5NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
6CLERK_SECRET_KEY="sk_test_..."
7NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in"
8NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up"
9NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/dashboard"
10NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/dashboard"
clerk_layout.tsx
TSX
1// app/layout.tsx
2import { ClerkProvider } from "@clerk/nextjs";
3
4export default function RootLayout({ children }: { children: React.ReactNode }) {
5 return (
6 <ClerkProvider>
7 <html lang="en">
8 <body>{children}</body>
9 </html>
10 </ClerkProvider>
11 );
12}
13
14// app/sign-in/[[...sign-in]]/page.tsx
15import { SignIn } from "@clerk/nextjs";
16
17export default function SignInPage() {
18 return <SignIn />;
19}
20
21// app/sign-up/[[...sign-up]]/page.tsx
22import { SignUp } from "@clerk/nextjs";
23
24export default function SignUpPage() {
25 return <SignUp />;
26}
27
28// components/Header.tsx
29import { UserButton, SignedIn, SignedOut } from "@clerk/nextjs";
30import Link from "next/link";
31
32export default function Header() {
33 return (
34 <header className="flex items-center justify-between p-4">
35 <Link href="/">Home</Link>
36 <SignedIn>
37 <UserButton />
38 </SignedIn>
39 <SignedOut>
40 <Link href="/sign-in">Sign in</Link>
41 </SignedOut>
42 </header>
43 );
44}

Clerk exposes an auth() helper for Server Components and route handlers. It returns an object with the user ID, session claims, and organization information. Use it to gate content, check roles, and personalize data fetching.

server_auth.tsx
TSX
1// app/dashboard/page.tsx
2import { auth, currentUser } from "@clerk/nextjs/server";
3import { redirect } from "next/navigation";
4
5export default async function DashboardPage() {
6 const { userId, sessionClaims } = await auth();
7
8 if (!userId) {
9 redirect("/sign-in");
10 }
11
12 const user = await currentUser();
13 const role = sessionClaims?.metadata?.role as string | undefined;
14
15 return (
16 <main>
17 <h1>Dashboard</h1>
18 <p>User: {user?.firstName}</p>
19 <p>Role: {role ?? "member"}</p>
20 </main>
21 );
22}
23
24// app/api/admin/route.ts
25import { auth } from "@clerk/nextjs/server";
26import { NextResponse } from "next/server";
27
28export async function GET() {
29 const { userId, sessionClaims } = await auth();
30
31 if (!userId) {
32 return new NextResponse("Unauthorized", { status: 401 });
33 }
34
35 const role = sessionClaims?.metadata?.role as string | undefined;
36 if (role !== "admin") {
37 return new NextResponse("Forbidden", { status: 403 });
38 }
39
40 return NextResponse.json({ secret: "admin-only data" });
41}

The Clerk middleware is where you enforce global route protection. Use the createRouteMatcherhelper to define public routes, and protect everything else by default. This matches the "deny by default" security posture: every route requires a session unless explicitly listed as public.

middleware.ts
TSX
1// middleware.ts
2import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
3
4const isPublicRoute = createRouteMatcher([
5 "/",
6 "/sign-in(.*)",
7 "/sign-up(.*)",
8 "/api/webhook(.*)",
9]);
10
11export default clerkMiddleware(async (auth, req) => {
12 if (!isPublicRoute(req)) {
13 await auth.protect();
14 }
15});
16
17export const config = {
18 matcher: [
19 "/((?!_next/static|_next/image|favicon.ico|.*\.\w+$).*)",
20 ],
21};
โš 

warning

Middleware runs on the edge and has limited access to Node.js APIs. Do not perform heavy database checks here. Use middleware only for lightweight checks, and defer detailed authorization to Server Components or route handlers.
Protecting Routes

Route protection in Next.js can happen at multiple layers. Middleware is ideal for coarse-grained access control: is the user signed in? Are they hitting an admin prefix? Server Components and route handlers are better for fine-grained decisions that depend on database state, such as ownership or subscription tier.

The middleware matcher is the most common source of protection bugs. A matcher that is too narrow leaves routes unprotected; a matcher that is too broad runs auth on every asset request, including static files and images. The standard Clerk matcher above excludes _next/static, _next/image, favicons, and files with explicit extensions.

middleware.ts
TSX
1// middleware.ts with role-based route protection
2import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
3
4const isPublicRoute = createRouteMatcher([
5 "/",
6 "/sign-in(.*)",
7 "/sign-up(.*)",
8 "/api/public(.*)",
9]);
10
11const isAdminRoute = createRouteMatcher(["/admin(.*)"]);
12
13export default clerkMiddleware(async (auth, req) => {
14 const { userId, sessionClaims } = await auth();
15
16 // Public routes need no further checks
17 if (isPublicRoute(req)) return;
18
19 // Any non-public route requires a session
20 if (!userId) {
21 return new Response("Unauthorized", { status: 401 });
22 }
23
24 // Admin routes require the admin role stored in session metadata
25 if (isAdminRoute(req)) {
26 const role = sessionClaims?.metadata?.role as string | undefined;
27 if (role !== "admin") {
28 return new Response("Forbidden", { status: 403 });
29 }
30 }
31});
32
33export const config = {
34 matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\.\w+$).*)"],
35};

Redirecting to sign-in is usually better than returning a 401 for page routes. A 401 is correct for APIs, but a browser user hitting a protected page should be sent somewhere useful. Always preserve the original URL as a callback so the user returns after signing in.

redirect_middleware.ts
TSX
1// middleware.ts with redirect to sign-in
2import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
3import { NextResponse } from "next/server";
4
5const isPublicRoute = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)"]);
6
7export default clerkMiddleware(async (auth, req) => {
8 const { userId } = await auth();
9
10 if (!isPublicRoute(req) && !userId) {
11 const signInUrl = new URL("/sign-in", req.url);
12 signInUrl.searchParams.set("redirect_url", req.url);
13 return NextResponse.redirect(signInUrl);
14 }
15});
16
17export const config = {
18 matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\.\w+$).*)"],
19};

In Server Components, you can check the role and ownership with database context. This is where you enforce rules like "only the project owner can delete it" or "only paid subscribers can access this feature." Keep the checks explicit and return early on failure.

page.tsx
TSX
1// app/projects/[id]/page.tsx
2import { auth } from "@clerk/nextjs/server";
3import { notFound, redirect } from "next/navigation";
4import { getProject } from "@/lib/projects";
5
6export default async function ProjectPage({
7 params,
8}: {
9 params: Promise<{ id: string }>;
10}) {
11 const { userId } = await auth();
12 if (!userId) redirect("/sign-in");
13
14 const { id } = await params;
15 const project = await getProject(id);
16
17 if (!project) notFound();
18
19 // Ownership check
20 if (project.ownerId !== userId) {
21 return (
22 <main>
23 <h1>Forbidden</h1>
24 <p>You do not have permission to view this project.</p>
25 </main>
26 );
27 }
28
29 return (
30 <main>
31 <h1>{project.name}</h1>
32 <p>Owner: {project.ownerId}</p>
33 </main>
34 );
35}

In Client Components, you can only check what the client already knows. The session data is derived from the server and exposed through a provider. Client-side guards are useful for UI affordances, but they must never be the only line of defense. Always re-verify permissions on the server.

client_guard.tsx
TSX
1// components/AdminLink.tsx
2"use client";
3
4import { useAuth } from "@clerk/nextjs";
5import Link from "next/link";
6
7export default function AdminLink() {
8 const { sessionClaims } = useAuth();
9 const role = sessionClaims?.metadata?.role as string | undefined;
10
11 if (role !== "admin") return null;
12
13 return <Link href="/admin">Admin Dashboard</Link>;
14}
15
16// app/dashboard/page.tsx with client and server guards
17import { auth } from "@clerk/nextjs/server";
18import AdminLink from "@/components/AdminLink";
19
20export default async function DashboardPage() {
21 const { userId } = await auth();
22 if (!userId) return null; // middleware already redirects, but be defensive
23
24 return (
25 <main>
26 <h1>Dashboard</h1>
27 <AdminLink />
28 </main>
29 );
30}
โœ“

best practice

Treat client-side role checks as UI hints, not security controls. Every protected action must be re-authorized on the server. Never trust the client to tell you its role.
Server Actions & Auth

Server Actions run on the server, so they have access to the same cookies and session data as route handlers and Server Components. The correct pattern is to call your auth helper inside the action and reject unauthorized requests immediately. This is where most form submissions are protected, because forms are the primary way users mutate state.

Auth.js actions import auth from auth.ts. Clerk actions import auth from @clerk/nextjs/server. The shape of the returned session differs, but the pattern is the same: verify, then act.

server_actions_authjs.ts
TSX
1// app/actions/post.ts
2"use server";
3
4import { auth } from "@/auth";
5import { revalidatePath } from "next/cache";
6import { createPostSchema } from "@/lib/validation";
7
8export async function createPost(formData: FormData) {
9 const session = await auth();
10
11 if (!session?.user) {
12 throw new Error("Unauthorized");
13 }
14
15 const data = createPostSchema.parse({
16 title: formData.get("title"),
17 content: formData.get("content"),
18 });
19
20 // Insert into database with the authenticated user as owner
21 await db.post.create({
22 data: {
23 title: data.title,
24 content: data.content,
25 authorId: session.user.id,
26 },
27 });
28
29 revalidatePath("/posts");
30}
31
32// app/posts/new/page.tsx
33import { createPost } from "@/app/actions/post";
34
35export default function NewPostPage() {
36 return (
37 <form action={createPost}>
38 <input name="title" placeholder="Title" required />
39 <textarea name="content" placeholder="Content" required />
40 <button type="submit">Publish</button>
41 </form>
42 );
43}
server_actions_clerk.ts
TSX
1// app/actions/clerk-post.ts
2"use server";
3
4import { auth } from "@clerk/nextjs/server";
5import { revalidatePath } from "next/cache";
6import { createPostSchema } from "@/lib/validation";
7
8export async function createPost(formData: FormData) {
9 const { userId } = await auth();
10
11 if (!userId) {
12 throw new Error("Unauthorized");
13 }
14
15 const data = createPostSchema.parse({
16 title: formData.get("title"),
17 content: formData.get("content"),
18 });
19
20 await db.post.create({
21 data: {
22 title: data.title,
23 content: data.content,
24 authorId: userId,
25 },
26 });
27
28 revalidatePath("/posts");
29}
30
31// Server Action with ownership check
32export async function deletePost(postId: string) {
33 const { userId } = await auth();
34 if (!userId) throw new Error("Unauthorized");
35
36 const post = await db.post.findUnique({ where: { id: postId } });
37 if (!post) throw new Error("Post not found");
38 if (post.authorId !== userId) throw new Error("Forbidden");
39
40 await db.post.delete({ where: { id: postId } });
41 revalidatePath("/posts");
42}

Server Actions are protected against cross-site request forgery by default because they require a same-origin request and a valid session. However, you should still validate and sanitize every input. Never trust the shape of data sent from a form, and never expose internal IDs or error messages that reveal the existence of resources.

โœ•

danger

Do not pass the user ID from the client to a Server Action and use it as the author. Always derive the authenticated user from the session on the server. A malicious client can send any user ID it wants.
JWT vs Session Strategy

Auth.js supports two session strategies: JSON Web Tokens stored in an encrypted cookie, and database sessions stored in a persistence layer. Clerk manages session state internally and exposes it through short-lived tokens. The choice of strategy affects latency, revocation, and complexity.

AspectJWTDatabase Session
StorageEncrypted cookie on the clientSession row in database
LatencyNo DB hit to verifyRequires DB lookup
RevocationHard to revoke until token expiresInstant by deleting session row
Size limitsLimited by cookie size (4KB)Only the session ID is in the cookie
Best forStateless, high-traffic APIsAdmin dashboards, sensitive operations
ScalingEasy, no shared storageRequires session store or database

In Auth.js, choose the strategy with the session.strategy option. The JWT strategy is the default and works well for most applications. Switch to database sessions when you need to revoke sessions, track active logins, or store large amounts of session metadata.

auth_database.ts
TSX
1// auth.ts with database session adapter
2import NextAuth from "next-auth";
3import Google from "next-auth/providers/google";
4import { PrismaAdapter } from "@auth/prisma-adapter";
5import { db } from "@/lib/db";
6
7export const { handlers, auth, signIn, signOut } = NextAuth({
8 adapter: PrismaAdapter(db),
9 providers: [Google],
10 session: {
11 strategy: "database", // Store session in the database
12 maxAge: 30 * 24 * 60 * 60,
13 updateAge: 24 * 60 * 60,
14 },
15 callbacks: {
16 session({ session, user }) {
17 session.user.id = user.id;
18 return session;
19 },
20 },
21});

Token lifecycle matters regardless of strategy. Keep access tokens short-lived, refresh tokens longer but rotatable, and session cookies scoped to your domain with the Secure, HttpOnly, and SameSite attributes set. Auth.js and Clerk set these defaults for you, but review them when you customize cookies.

โ„น

info

For maximum security, combine short-lived JWTs with a server-side revocation list. You get the speed of JWTs and the ability to revoke sessions immediately by checking the list on each request.
Best Practices

1. Never expose secrets to the client. Keep provider secrets, API keys, and session signing keys in environment variables that are only read server-side.

2. Use environment variables through process.env in server code. Never import or reference secrets in client components, and do not prefix them with NEXT_PUBLIC_.

3. Validate all inputs on the server, including data from forms, query parameters, and request bodies. Zod is a solid choice for TypeScript schemas.

4. Use HTTPS in production so cookies and tokens cannot be intercepted in transit. Local development should use a trusted TLS proxy for production parity.

5. Keep tokens short-lived. An access token should last minutes, not days. Long-lived sessions can be refreshed securely with rotating refresh tokens.

6. Secure cookies with HttpOnly, Secure, and SameSite=Lax or Strict. Never store tokens in localStorage.

7. Use middleware for global, coarse-grained protection. Use Server Components and route handlers for fine-grained, database-backed authorization.

8. Run authorization checks as close to the data as possible. The further from the database the check is, the easier it is to bypass with a new code path.

9. Log authentication failures, suspicious IPs, and role escalation attempts. Security is not just prevention; it is also detection.

10. Rotate keys periodically. Auth.js and Clerk both support key rotation without invalidating active user sessions.

Common Pitfalls

1. Calling auth on the client only. A client-side if (!user) redirect() check does not protect anything. The server must enforce the rule.

2. Storing secrets in client code. Anything in a browser bundle can be read by a user. Keep secrets in server-side code and environment variables.

3. Missing or incorrect middleware matcher. An overly broad matcher slows your app; a missing matcher leaves routes open. Test the matcher against your actual route list.

4. Race conditions in auth state. Client-side checks can run before the session has loaded. Use the loading state and always verify on the server before mutations.

5. Trusting roles from the client. Session claims or roles can be tampered with if sent from the client. Re-read the role from the server session on every request.

6. Using unencrypted cookies. If you set your own cookies, encrypt them. Auth.js and Clerk handle this, but custom solutions must be audited.

7. Forgetting CSRF protection for custom forms. Server Actions include CSRF protection by default, but custom API endpoints and external form submissions need explicit guards.

8. Leaking user existence through error messages. Use generic messages like "Invalid credentials" instead of "User not found" to prevent user enumeration.

9. Overloading JWT payloads. Cookies have a 4KB limit in most browsers. Keep JWT claims small and fetch heavy data from the database when needed.

10. Skipping input validation. Auth checks are not a substitute for validation. Malformed input can still crash your application or lead to injection attacks.

๐Ÿ”ฅ

pro tip

When in doubt, add the auth check to the server path closest to the data. Every route handler, Server Action, and Server Component that touches sensitive data should independently verify the user. Defense in depth beats a single front door.
$Blueprint โ€” Engineering DocumentationยทSection ID: NEXT-AUTHยทRevision: 1.0