Most auth tutorials show you a login form. Few show you what happens after: how the session actually gets verified, or why checking a session at the edge looks nothing like verifying it inside your server.
I've been building a paid Next.js 16 Auth Kit, and while writing the docs for it, I kept circling back to that gap. So I pulled the session layer out and released it separately, free.
What's in it:
cookies() APIproxy.ts runs on the edge and checks if a session cookie exists. dashboard/layout.tsx performs full JWT verification, signature, expiry, and payload shape, before rendering protected pages. If one layer misses something, the other catches itlib/env.ts checks DATABASE_URL and JWT_SECRET at module load and throws immediately if either is missingany types, plus a server-only guard so auth code can't accidentally be imported into a Client ComponentNo email verification or password reset in this free version, and that's deliberate. It's meant to teach the session layer, not be a complete authentication system.
Product page: https://shubhra.dev/products/nextjs-16-auth-scaffold
Repo: https://github.com/shubhrapokhariya/nextjs-16-auth-scaffold
Live demo: https://auth-scaffold.shubhra.dev/
If you need OAuth, email verification, password reset, roles, or an admin panel on top of this, that's what my paid Auth Kit adds. But this scaffold works fully standalone if session auth is all you need.
What are you using for auth in Next.js 16 right now? Still rolling your own, or have you moved to Clerk or Auth.js?
The edge check/full verification split is the right teaching point, but 'if one layer misses something' could imply defense in depth where the edge layer is only a routing hint. I'd document that proxy.ts must never authorize data access; only the verified server-side session may do that. A forged or expired cookie should reach the layout and fail closed in the demo.
In the free scaffold, proxy.ts is strictly a routing hint. It checks if the session cookie exists, and if it doesn't, it redirects to /login. That's it. It never touches the JWT, never verifies the signature, and never authorizes anything.
A forged or expired cookie gets past proxy.ts every time. It only fails closed in dashboard/layout.tsx, where the actual JWT verification happens. So proxy.ts catches "no cookie at all," but layout.tsx catches everything else.
My "if one layer misses something" line was sloppy. In the free scaffold, there's really only one security layer for session validity: the layout. The proxy is just a cheap gate to skip server work for unauthenticated requests.
(The paid kit does add a third layer where the data layer scopes queries to the requesting user, but you're right that for the free scaffold, the layout is the only gate that matters.)
That boundary is much clearer. I'd encode it directly in tests: no cookie redirects at proxy.ts, while forged and expired cookies pass the proxy and fail in dashboard/layout.tsx. That turns the routing-hint distinction into an invariant a future refactor can't quietly erase.
That's exactly the behavior I want guaranteed. Framing it as an explicit invariant is a sharper way to think about it. The boundary between a routing hint and a security gate is easy to blur in documentation, and expressing it through tests makes it much harder to accidentally break during future refactors. Thanks for the suggestion.
That is the clean outcome: one documented boundary, one invariant, and no fake defense-in-depth story. The useful test is not only that forged cookies fail today, but that a future contributor cannot move authorization into proxy.ts without breaking CI. Nice correction.