Protect pages and APIs
Decide which routes require a signed-in user, and what happens when there isn't one.
There are three layers of protection. You'll usually use the first, and sometimes add the others.
| Layer | Where | Good for |
|---|---|---|
authProxy |
proxy.ts / middleware.ts |
Whole sections of your site (e.g. everything under /dashboard) |
auth() |
Server Components, Route Handlers, Server Actions | Per-route checks and getting the userId |
<SignedIn> / <SignedOut> |
Client components | Showing or hiding UI. Not a security boundary. |
Protect sections with authProxy
// proxy.ts
import { authProxy } from "@daywalker/auth/nextjs";
export default authProxy({
protect: [
"/dashboard*", // /dashboard and everything below it
"/settings", // exactly /settings
/^\/projects\/[^/]+\/edit$/, // regular expressions work too
"/api/private*",
],
});
export const config = { matcher: ["/((?!_next|favicon.ico).*)"] };Matching rules:
- A string ending in
*matches that prefix. - Any other string must match the path exactly.
- A
RegExpis tested against the path, without the query string.
What happens to signed-out requests:
- Page requests are redirected to the hosted sign-in page, with a
redirect_urlthat brings the user back. - API requests get
401with a JSON body. A request counts as an API request if its path starts with/apior it sendsAccept: application/json.
Using your own sign-in page
If you built your own form (see Build your own sign-in form), point the proxy at it:
export default authProxy({ protect: ["/dashboard*"], signInUrl: "https://app.example.com/login" });Check inside a route with auth()
Even with the proxy in place, always check auth() in API routes and Server Actions that change data. It's cheap, and it keeps the route safe if someone later edits the proxy's list.
// app/api/notes/route.ts
import { auth } from "@daywalker/auth/nextjs";
export async function POST(req: Request) {
const { userId } = await auth();
if (!userId) return Response.json({ error: "Not signed in" }, { status: 401 });
const { text } = await req.json();
const note = await db.notes.create({ ownerId: userId, text });
return Response.json(note, { status: 201 });
}// app/notes/actions.ts
"use server";
import { auth } from "@daywalker/auth/nextjs";
export async function deleteNote(noteId: string) {
const { userId } = await auth();
if (!userId) throw new Error("Not signed in");
// Scope by owner, so users can't delete each other's notes by guessing IDs
await db.notes.delete({ where: { id: noteId, ownerId: userId } });
}Warning Checking that someone is signed in is authentication. You still need authorization: checking that this user may touch this record. Always scope database queries by userId, as above.
Hide UI on the client
"use client";
import { SignedIn, SignedOut } from "@daywalker/auth/react";
export function Pricing() {
return (
<>
<SignedOut><a href="/sign-up">Start free</a></SignedOut>
<SignedIn><a href="/dashboard">Go to your dashboard</a></SignedIn>
</>
);
}Client-side checks only change what's shown. Anyone can edit JavaScript in their browser, so never rely on them to protect data. That's what the server checks above are for.