Authentication asks who the visitor is; authorization asks whether this visitor may touch this record. The second question has to be answered where the record is loaded, the only place every caller passes through. The pattern Next.js 10,514 recommends is a data access layer: a server-only module that verifies the session, queries, and returns a trimmed object.
import "server-only";
export const getBooksForUser = cache(async () => {
const { userId } = await verifySession();
const rows = await books.find({ ownerId: userId }).toArray();
return rows.map((b) => ({ id: String(b._id), title: b.title }));
});React 7,897 's cache() memoizes the call for one render pass, so a page, a layout and three components can each call verifySession() and the cookie is unsealed once. Returning {id, title} is the other half: a Server Component that hands a whole document to a Client Component has published every field in it.
The demo application has two users, ada (admin) and lem (reader). The dashboard renders a delete button only for admins; the button calls a Server Action that checks the role again.
// app/dashboard/page.tsx
const { role } = await verifySession();
return <>{role === "admin" && <DangerButton />} ...</>;
// app/actions.ts
export async function deleteAllBooks() {
const { userId, role } = await verifySession();
if (role !== "admin")
return { ok: false, error: `403 Forbidden: ${userId} is not an admin` };
return { ok: true, message: `Deleted every book (requested by ${userId})` };
}
Signed in as lem that button is not in the HTML at all. But every Server Action has an id, and Next.js prints those ids into the client bundle it does send; posting one back to the route invokes the action directly. Below, $LEM holds the reader's cookie value.
curl -s localhost:3319/dashboard -H "Cookie: bookshelf_session=$LEM" | grep -c "Delete every"
curl -s -X POST localhost:3319/dashboard -H "Origin: http://localhost:3319" \
-H "Cookie: bookshelf_session=$LEM" -H "Content-Type: text/plain;charset=UTF-8" \
-H "Next-Action: 004391bdaa2249bd49db608c1a57b28ec282312189" --data '[]' | tail -10
1:{"ok":false,"error":"403 Forbidden: lem is not an admin"}Zero occurrences of the button, and the action ran anyway. What stopped it was the check inside the action, not the page and not the proxy, which saw a signed-in user asking for a page they may see. Action ids are encrypted and recalculated on each build, but that is an obstacle, not a boundary.