diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000000..82c8143949 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2026-04-23 - [Request-level caching with React 'cache'] +**Learning:** React's `cache()` is an essential tool for deduplicating database queries and expensive CPU operations (like JWT verification) within a single request lifecycle in Next.js. However, it is not available in the Edge Runtime/Middleware. +**Action:** Use `cache()` to wrap core data-fetching and auth-verification functions, but always provide a fallback check `(typeof cache === 'function' ? cache : (fn) => fn)` when those functions are shared with Middleware. diff --git a/lib/auth/session.ts b/lib/auth/session.ts index 0c474c70aa..a96027ba02 100644 --- a/lib/auth/session.ts +++ b/lib/auth/session.ts @@ -2,6 +2,7 @@ import { compare, hash } from 'bcryptjs'; import { SignJWT, jwtVerify } from 'jose'; import { cookies } from 'next/headers'; import { NewUser } from '@/lib/db/schema'; +import { cache } from 'react'; const key = new TextEncoder().encode(process.env.AUTH_SECRET); const SALT_ROUNDS = 10; @@ -30,12 +31,17 @@ export async function signToken(payload: SessionData) { .sign(key); } -export async function verifyToken(input: string) { - const { payload } = await jwtVerify(input, key, { - algorithms: ['HS256'], - }); - return payload as SessionData; -} +// ⚡ OPTIMIZATION: Memoize verifyToken to deduplicate JWT verification within a single request. +// This is especially useful when multiple server components or functions call verifyToken. +// We use a conditional check for 'cache' because it's not available in the Edge Runtime (Middleware). +export const verifyToken = (typeof cache === 'function' ? cache : (fn: any) => fn)( + async (input: string) => { + const { payload } = await jwtVerify(input, key, { + algorithms: ['HS256'], + }); + return payload as SessionData; + } +); export async function getSession() { const session = (await cookies()).get('session')?.value; diff --git a/lib/db/queries.ts b/lib/db/queries.ts index c607c8f746..0ee8030e97 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -3,8 +3,10 @@ import { db } from './drizzle'; import { activityLogs, teamMembers, teams, users } from './schema'; import { cookies } from 'next/headers'; import { verifyToken } from '@/lib/auth/session'; +import { cache } from 'react'; -export async function getUser() { +// ⚡ OPTIMIZATION: Memoize getUser to deduplicate database queries within a single request. +export const getUser = cache(async () => { const sessionCookie = (await cookies()).get('session'); if (!sessionCookie || !sessionCookie.value) { return null; @@ -34,7 +36,7 @@ export async function getUser() { } return user[0]; -} +}); export async function getTeamByStripeCustomerId(customerId: string) { const result = await db @@ -64,7 +66,8 @@ export async function updateTeamSubscription( .where(eq(teams.id, teamId)); } -export async function getUserWithTeam(userId: number) { +// ⚡ OPTIMIZATION: Memoize getUserWithTeam to deduplicate database queries within a single request. +export const getUserWithTeam = cache(async (userId: number) => { const result = await db .select({ user: users, @@ -76,7 +79,7 @@ export async function getUserWithTeam(userId: number) { .limit(1); return result[0]; -} +}); export async function getActivityLogs() { const user = await getUser(); @@ -99,7 +102,8 @@ export async function getActivityLogs() { .limit(10); } -export async function getTeamForUser(userId: number) { +// ⚡ OPTIMIZATION: Memoize getTeamForUser to deduplicate database queries within a single request. +export const getTeamForUser = cache(async (userId: number) => { const result = await db.query.users.findFirst({ where: eq(users.id, userId), with: { @@ -126,4 +130,4 @@ export async function getTeamForUser(userId: number) { }); return result?.teamMembers[0]?.team || null; -} +});