diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000000..7b65efe985 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,5 @@ +# Bolt's Performance Journal + +## 2026-04-23 - Memoizing Authentication and Data Fetching +**Learning:** In Next.js App Router, core functions like `getUser` and `verifyToken` are often called by multiple server components and layouts. Without memoization, this leads to redundant database queries and CPU-intensive JWT verification. React's `cache()` is the recommended way to deduplicate these operations within a single request. +**Action:** Always wrap shared data-fetching or expensive utility functions with `cache()` to ensure they only run once per request. diff --git a/lib/auth/session.ts b/lib/auth/session.ts index 0c474c70aa..e752632906 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,16 @@ export async function signToken(payload: SessionData) { .sign(key); } -export async function verifyToken(input: string) { +/** + * ⚡ OPTIMIZATION: Memoized JWT verification. + * Prevents redundant CPU-intensive JWT verification within a single request lifecycle. + */ +export const verifyToken = cache(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..55a9ee7608 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -3,8 +3,14 @@ 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: Memoized user lookup. + * Ensures the database is queried only once per request, even if multiple + * components call this function. Reduces database load and request latency. + */ +export const getUser = cache(async () => { const sessionCookie = (await cookies()).get('session'); if (!sessionCookie || !sessionCookie.value) { return null; @@ -34,7 +40,7 @@ export async function getUser() { } return user[0]; -} +}); export async function getTeamByStripeCustomerId(customerId: string) { const result = await db @@ -64,7 +70,12 @@ export async function updateTeamSubscription( .where(eq(teams.id, teamId)); } -export async function getUserWithTeam(userId: number) { +/** + * ⚡ OPTIMIZATION: Memoized user and team lookup. + * Deduplicates database queries when retrieving both user and team info + * in the same request. + */ +export const getUserWithTeam = cache(async (userId: number) => { const result = await db .select({ user: users, @@ -76,7 +87,7 @@ export async function getUserWithTeam(userId: number) { .limit(1); return result[0]; -} +}); export async function getActivityLogs() { const user = await getUser(); @@ -99,7 +110,11 @@ export async function getActivityLogs() { .limit(10); } -export async function getTeamForUser(userId: number) { +/** + * ⚡ OPTIMIZATION: Memoized team lookup for user. + * Prevents redundant complex joins/queries for team data 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 +141,4 @@ export async function getTeamForUser(userId: number) { }); return result?.teamMembers[0]?.team || null; -} +});