
Nextjs Cache Architecture
Structure Next.js cache tags and centralized `updateTag` revalidation so mutations never call cache APIs ad hoc.
Overview
Nextjs-cache-architecture is an agent skill most often used in Build (also Ship, Operate) that standardizes Next.js cache tags and centralized `updateTag` revalidation for list and detail views.
Install
npx skills add https://github.com/mohamed-hossam1/nextjs-cache-architecture --skill nextjs-cache-architectureWhat is this skill?
- Centralizes every `updateTag()` call in one `revalidate.ts` module mutations import
- Documents bulk vs surgical invalidation: collection tag plus optional per-entity tag factory
- Single `updateTags()` helper enables one observability hook for all invalidation events
- Includes `SuspenseOnSearchParams` pattern so fallbacks re-trigger when search params change
- Keeps mutations focused on data changes instead of scattering cache mechanics
- 2 revalidation tiers documented: bulk collection and surgical per-entity with parent list invalidation
Adoption & trust: 1.8k installs on skills.sh; 7 GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
Your Next.js mutations call `updateTag` in random places, so cache bugs are hard to audit and list pages stay stale after single-record updates.
Who is it for?
Solo builders on Next.js App Router with server actions or mutations that need predictable tag-based cache invalidation.
Skip if: Static sites with no tagged server data, or teams already standardized on a different cache layer (e.g. pure client SWR with no Next cache tags).
When should I use this skill?
Designing or refactoring Next.js tagged caching and server-side revalidation for App Router apps.
What do I get? / Deliverables
You get a tags module, centralized revalidate exports, and Suspense patterns so every write invalidates the right collection and entity tags from one auditable path.
- CACHE_TAGS module
- Centralized revalidate.ts with bulk and surgical helpers
- SuspenseOnSearchParams UI boundary pattern
Recommended Skills
Journey fit
Spans multiple journey phases - primary shelf plus alternate fits below.
Canonical shelf is build/frontend because the skill defines tag factories, server actions, and Suspense patterns while implementing data-heavy Next.js UI. Caching and `revalidatePath`-style invalidation live in the Next.js rendering and data-fetch layer, not in infra monitoring.
Where it fits
Add `revalidate[Entity]Cache(id)` that invalidates both entity and parent collection tags after an edit form submits.
Instrument the shared `updateTags` function before launch to verify invalidation volume under load tests.
Audit all mutation imports point at `lib/cache/revalidate.ts` when users report stale list rows.
How it compares
Use as an architecture template instead of copying `updateTag` into every server action by hand.
Common Questions / FAQ
Who is nextjs-cache-architecture for?
Indie developers building Next.js SaaS UIs who use `next/cache` tags and want one place to manage invalidation.
When should I use nextjs-cache-architecture?
During Build when wiring CACHE_TAGS and revalidate helpers; during Ship when tuning perf before launch; during Operate when debugging stale dashboards after production writes.
Is nextjs-cache-architecture safe to install?
It is documentation and code patterns only—review the Security Audits panel on this page and treat generated server code like any other project change.
SKILL.md
READMESKILL.md - Nextjs Cache Architecture
// lib/cache/revalidate.ts // // Every `updateTag()` call in the project lives here. // Mutations import these functions — they never call `updateTag()` directly. // // Why centralize: // - One place to audit invalidation behavior. // - One place to change the strategy (e.g., add observability). // - Mutations stay focused on data changes, not cache mechanics. "use server"; import { updateTag } from "next/cache"; import { CACHE_TAGS } from "./tags"; function updateTags(tags: string[]) { // Optional: add observability here. Because every invalidation flows // through this one function, a single console.log / OpenTelemetry span // gives full coverage with no per-call-site instrumentation. // Example: // console.log("[cache] updateTag", { tags, at: new Date().toISOString() }); for (const tag of tags) updateTag(tag); } // Bulk — any entry in the collection changed. // Replace [Collection] / [collection] with the user's real names. export async function revalidate[Collection]Cache() { updateTags([CACHE_TAGS.[collection]]); } // Surgical — one specific entry changed. // Only export this if `CACHE_TAGS.[entity]` factory exists in tags.ts. // Always invalidate the parent collection too — list views read from it. export async function revalidate[Entity]Cache(id: string | number) { updateTags([ CACHE_TAGS.[collection], CACHE_TAGS.[entity](id), ]); } // components/SuspenseOnSearchParams.tsx // // A standard <Suspense> does NOT re-trigger its fallback on client-side // navigation when only `searchParams` changes — the boundary is keyed on // the route, not the query string. This wrapper re-keys the boundary on // every searchParams change so the fallback shows during the new fetch. // // Use this on every page that has search or filter params. "use client"; import { useSearchParams } from "next/navigation"; import { Suspense, type ReactNode } from "react"; type Props = { fallback: ReactNode; children: ReactNode; }; export default function SuspenseOnSearchParams({ fallback, children }: Props) { const searchParams = useSearchParams(); return ( <Suspense key={searchParams.toString()} fallback={fallback}> {children} </Suspense> ); } // lib/cache/tags.ts // // Single source of truth for every cache tag in the project. // Raw tag strings are never written anywhere else. // // Conventions: // - Lowercase only. // - Entity tags use `domain:id` format. // - Match the actual data model — do not invent names. // - Add an entity factory ONLY when a mutation needs `updateTag()` // on a single entry. Otherwise the collection tag is enough. // // Type safety: // `as const satisfies TagRegistry` keeps every value literal-typed // (so the compiler knows `CACHE_TAGS.posts` is exactly `"posts"`) // AND enforces that every entry is either a tag string or a factory // that returns one. Misuse — e.g. dropping in a number, an object, // or a function with the wrong signature — fails to compile. type Tag = string; type EntityTagFactory = (id: string | number) => Tag; type TagRegistry = Record<string, Tag | EntityTagFactory>; export const CACHE_TAGS = { // COLLECTION TAGS — one per logical data group, always present. // Replace [collection] with the user's real collection name. [collection]: "[collection]", [anotherCollection]: "[anotherCollection]", // ENTITY TAG FACTORIES — only when a mutation targets a single entry. // Replace [entity] with the user's real entity name. [entity]: ((id) => `[entity]:${id}`) satisfies EntityTagFactory, } as const satisfies TagRegistry; { "skill_name": "nextjs-cache-architecture", "evals": [ { "id": 1, "name": "greenfield-posts-with-comments", "prompt": "I'm building a blog in Next.js 16 (App Router). I have a `posts` table and a `comments` table — every post has many comments. I need a posts listing page at /posts, a post detail page at /posts/[slug] showing the post and its comments, and a da