Verified against Claude Code · 2026-08-03
Wire Auth.js session checks correctly across Server Components, Route Handlers, and middleware
Splits Auth.js session verification by layer — a light JWT check in middleware versus a full session load in a Server Component — and flags the specific database-session-strategy trap that breaks in edge middleware.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are wiring Auth.js (NextAuth) session checks across a Next.js App Router app, deciding what runs in middleware versus what runs in a Server Component or Route Handler, and specifically checking whether the session strategy in use is even compatible with where each check is being placed. SESSION STRATEGY JWT strategy — sessions are signed tokens stored in a cookie, no database lookup needed to verify signature and expiry ROUTES NEEDING PROTECTION /dashboard/* requires any signed-in session; /dashboard/admin/* additionally requires an admin role CURRENT AUTH CHECKS middleware currently calls auth() directly and checks session.user.role === 'admin' inside it — this works today but the team is considering switching to the database session strategy for revocation support ROLE OR PERMISSION MODEL A jwt callback attaches role to the token at sign-in, and a session callback copies it onto session.user.role DESIGN RULES Confirm the session strategy before deciding what middleware can actually check: a JWT session strategy encodes the session directly in a signed, stateless token, which middleware running on the edge runtime can verify on its own with no database call, but a database session strategy stores the session server-side and requires a lookup to confirm it's still valid — if the app uses the database strategy, middleware genuinely cannot verify a full, current session on its own without adding a database round trip to every matched request, which reintroduces the exact latency problem middleware exists to avoid; in that case, middleware should either not attempt full session verification at all, or the app should use a lighter, edge-compatible check (verifying only that a session cookie exists and is well-formed) in middleware while the actual, authoritative check happens in a Server Component or Route Handler that does have full database access. Use the auth() helper (or the session-reading utility Auth.js exposes for this) inside Server Components and Route Handlers for the authoritative check, reading the full session including whatever role or permission data the app's callbacks attach to it, since this is where a real database lookup, if the strategy requires one, is actually affordable — it runs once per request that reaches this layer, not on every request including ones middleware would otherwise reject before they get here. Keep role and permission logic out of middleware entirely unless the session strategy and the specific role data genuinely allow a stateless check — middleware's job here is presence and basic validity of a session, not authorization decisions that depend on data which may need a fresh lookup to be trustworthy, such as a role that could have changed since the token was issued. Apply the same session-reading pattern consistently across every protected Server Component and Route Handler rather than letting different routes call auth() with slightly different assumptions about what fields are guaranteed to exist on the returned session object — a route that assumes session.user.role is always present will throw or misbehave the moment it's hit by a session shape that predates a schema change, if that possibility hasn't been guarded against explicitly. OUTPUT FORMAT A table: Layer (middleware, Server Component, Route Handler) | What it checks | Why that check is affordable at this layer given the session strategy | What it explicitly does NOT check. Then one paragraph stating plainly whether the current session strategy and the current middleware implementation are actually compatible, or whether the middleware is attempting something the chosen strategy can't support on the edge runtime.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
The session-strategy distinction is the single fact that decides whether a given auth check is even architecturally possible at the layer it's placed in, not just whether it's a good idea: a JWT session is a signed, self-contained token that middleware running on the edge runtime can verify with no external lookup, while a database session strategy's whole point is that the session can be revoked or updated server-side, which structurally requires a database round trip to confirm current validity — no amount of clever middleware code makes that check free on the edge runtime, because the constraint isn't about code quality, it's about what data is physically available without a network call. This is exactly why a role check running directly inside middleware today, using session.user.role read off a JWT, quietly stops being safe to run there the moment the strategy switches to database-backed sessions — the middleware code doesn't change, doesn't error, and doesn't warn anyone, it just becomes a decision made on data that might already be stale relative to what the database would say, since a revoked or role-changed session issued before the switch might still carry an old JWT until it naturally expires. Separating "does a session exist and look well-formed" from "is this specific role or permission still current" gives each layer a check it's actually equipped to answer correctly: middleware's cheap, stateless check is well-suited to presence and basic validity, while a Server Component's or Route Handler's ability to hit the database on every request that actually reaches it, at a request volume already filtered down by middleware's cheap gate, is exactly where an authoritative, potentially-database-backed check belongs without the latency cost of running it on every request unconditionally. Requiring the same session-reading assumptions across every protected route closes a gap that tends to open gradually as an app grows — an early route written when every session guaranteed a role field, and a later route written after a schema change made that field optional for a new user type, can both compile and both look correct individually while actually disagreeing about what a session object is allowed to look like, a mismatch that only surfaces as a runtime error for the specific user whose session doesn't match whichever route's unstated assumption happens to be wrong.
What you get back
Layer | Checks | Why affordable here | Does NOT check middleware | session cookie exists and is a well-formed JWT with a valid signature and unexpired timestamp | Stateless verification, no database call, safe on the edge runtime for every matched request | Whether the role encoded in the token is still current, or whether the session has been revoked server-side Server Component (app/dashboard/admin/layout.tsx) | Full session via auth(), including session.user.role, checked against the required admin role | Runs once per request that already passed middleware's cheap gate — affordable to do a real, current check here | N/A — this is the authoritative check for this route
Compatibility: the current setup is consistent because the strategy is JWT-based, so middleware's role check is reading data that's genuinely as current as the token itself. If the strategy switches to database sessions as the team is considering, the role check inside middleware would need to move to the Server Component layer, since middleware would no longer be able to confirm the role is current without a database call it isn't equipped to make.
Verified against
Claude Code Sonnet 4.6 · 2026-08-03
Claude Sonnet 4.6 · 2026-08-08
Changelog
- 2026-08-03 — Initial publish, verified against Claude Code and Claude (Sonnet 4.6) on a Next.js 16 app using Auth.js with a JWT session strategy.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
