Keep Per-User Data Out of Shared Caches in Next.js

Finds per-user data kept in module state, under cache keys that omit the user, or in public CDN responses, plus mutations that skip revalidation.

What it flags

Flaggedlib/current-user.ts
import 'server-only';
import { auth } from '@/lib/auth';
import { db, type User } from '@/lib/db';

// Module scope is shared by every request this server instance handles
let currentUser: User | null = null;

export async function getCurrentUser() {
  if (currentUser) return currentUser;

  const session = await auth();
  if (!session?.user) return null;

  currentUser = await db.user.findUnique({ where: { id: session.user.id } });
  return currentUser;
}
Passeslib/current-user.ts
import 'server-only';
import { cache } from 'react';
import { auth } from '@/lib/auth';
import { db } from '@/lib/db';

export const getCurrentUser = cache(async () => {
  const session = await auth();
  if (!session?.user) return null;

  return db.user.findUnique({ where: { id: session.user.id } });
});

Why it matters

Next.js server code runs in long-lived processes that serve many users, and its caches outlive the request that filled them. A current-user variable at module scope, a cache key without the user ID, or a public Cache-Control header on an account endpoint hands one person's data to the next visitor, and the bug only appears once several users hit the same server instance or cache, never on a single-user development machine. The opposite failure is a mutation that never invalidates the cached route, so users save a change and keep seeing the old value until the cache expires.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Keep Per-User Data Out of Shared Caches in Next.js

Applies to `**/*.ts`, `**/*.tsx`, `next.config.*`.

Review Next.js server code, caches, and mutations for per-user data served from shared caches and for cached routes left stale after writes.

Flag changes that:
- Store request-scoped data (current user, session, tenant, locale, auth token) in module-level variables, singletons, or Maps in server code, which every request on that server instance shares.
- Cache per-user or per-tenant results with unstable_cache, a Redis or KV helper, or a similar key-based cache when the user or tenant ID is missing from the key; unstable_cache does not add closure variables to its key.
- Set Cache-Control: public or s-maxage on responses that depend on cookies, the session, or the Authorization header, whether in Route Handlers, proxy or middleware, or next.config headers().
- Mutate data in a Server Action or Route Handler without calling revalidatePath, updateTag, revalidateTag, or the project's invalidation helper for the cached routes that display that data.
- Call revalidateTag(tag, 'max') in a Server Action whose user expects to see the change at once; 'max' serves stale data while revalidating, and updateTag expires it immediately.
- Revalidate one literal path, such as revalidatePath('/posts'), when the same data also appears on other cached routes such as detail pages or dashboards; a literal path refreshes only that page.
- Call revalidatePath or revalidateTag after redirect(); redirect throws, so the revalidation never runs.

Do not flag:
- React cache() and other per-request memoization, which is scoped to one request.
- Cached functions whose arguments or key parts include the user or tenant ID.
- Public data that is identical for every visitor, such as marketing pages, docs, or a product catalog.
- Writes to data that no cached or statically rendered route displays.

Checks files matching **/*.ts, **/*.tsx, next.config.*.

More Next.js agents

See all Next.js code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code