From d28dde459af36c60a372a958b263b84645242cc2 Mon Sep 17 00:00:00 2001 From: openhands Date: Sun, 18 Oct 2026 14:49:34 +0200 Subject: [PATCH] perf(auth): memoize auth() per request with React cache A single authenticated render called auth() 4-5 times (access guard, site layout, both permission loaders, the page itself), and every call re-parsed the session cookie, decoded and HMAC-verified the JWT and ran the jwt/session callbacks. Wrapping NextAuth's handler in React's cache scopes that work to one verification per request. Outside a render (scripts, jobs worker) React's cache is a pass-through, so those call sites behave exactly as before. --- src/lib/auth.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 48eacda0..a3364224 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,6 +1,7 @@ import { eq } from "drizzle-orm"; import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; +import { cache } from "react"; import { env } from "@/env"; import { getCachedJwtVersion } from "@/lib/auth/jwt-version-cache"; import { @@ -25,7 +26,7 @@ import { db, User } from "@/lib/db"; import { logger } from "@/lib/logger"; import { clientIp, rateLimit } from "@/lib/rate-limit"; -export const { handlers, signOut, auth } = NextAuth({ +const nextAuth = NextAuth({ trustHost: true, secret: env.AUTH_SECRET, session: { strategy: "jwt", maxAge: 24 * 60 * 60 }, @@ -161,3 +162,15 @@ export const { handlers, signOut, auth } = NextAuth({ }, }, }); + +export const { handlers, signOut } = nextAuth; + +/** + * `auth()` re-parses the session cookie, decodes and HMAC-verifies the JWT and + * runs the `jwt`/`session` callbacks on every call. A single authenticated + * render calls it from the access guard, the site layout, both permission + * loaders and the page itself — 4-5 times on the critical path. React's + * `cache` scopes the memo to the current request, so those all share one + * verification. Outside a render (scripts, worker) it is a pass-through. + */ +export const auth = cache(nextAuth.auth);