Cache Tag Convention
Next.js revalidateTag is used alongside
revalidatePath to invalidate cached data after mutations in the web app.
This document defines the tag naming convention, the data sources that are
tagged, and the server actions responsible for invalidation.
Tag naming convention
All tags follow a two-level hierarchy that keeps invalidation tenant-isolated:
tenant-scoped list → tenant:{tenantId}:{collection}
entity-scoped detail → tenant:{tenantId}:{entity}:{id}Rules
-
Always prefix with
tenant:{tenantId}.
This ensures that invalidating one tenant’s data never affects another tenant’s cache. -
List tags (e.g.
tenant:abc:service-leads) cover all records in a collection for a tenant. Invalidate them after any create, update, or delete that changes list views. -
Entity tags (e.g.
tenant:abc:service-lead:123) cover a single document. Invalidate them after any write that touches that specific record. -
A mutation should invalidate both the entity tag and the list tag so that detail and list views stay consistent.
Tag reference
Helpers are defined in apps/web/lib/cache-tags.ts:
| Helper | Tag produced | Scope |
|---|---|---|
cacheTags.serviceLeadList(tenantId) | tenant:{tenantId}:service-leads | All leads for a tenant |
cacheTags.serviceLead(tenantId, id) | tenant:{tenantId}:service-lead:{id} | One service lead |
cacheTags.proposalList(tenantId) | tenant:{tenantId}:proposals | All proposals for a tenant |
cacheTags.proposal(tenantId, id) | tenant:{tenantId}:proposal:{id} | One proposal |
Tagged data sources
Data is tagged at fetch time via unstable_cache wrappers in apps/web/lib/queries/:
| Query helper | File | Tags applied |
|---|---|---|
getLeadById(tenantId, id) | lib/queries/service-leads.ts | serviceLead, serviceLeadList |
getProposalById(tenantId, id) | lib/queries/proposals.ts | proposal, proposalList |
Invalidation call sites
Service leads
| Server action | File | Tags invalidated |
|---|---|---|
updateLeadOverview | components/service-leads/lead-overview/actions.ts | serviceLead(tenantId, leadId), serviceLeadList(tenantId) |
The action also calls revalidatePath so that any sub-components that fetch
independently (e.g. LeadActivity, LeadProgress) receive fresh data when
the route re-renders.
Proposals
| Server action | File | Tags invalidated |
|---|---|---|
createProposal | app/(app)/proposals/create/actions.ts | proposalList(tenantId) |
updateProposal | app/(app)/proposals/create/actions.ts | proposal(tenantId, id), proposalList(tenantId) |
How it works end-to-end
User edits lead description
→ updateLeadOverview (Server Action)
→ leadService.update(...) ← writes to MongoDB
→ revalidateTag(serviceLead tag) ← marks cached getLeadById stale
→ revalidateTag(serviceLeadList) ← marks list cache stale
→ revalidatePath(...) ← invalidates full RSC route cache
→ Next.js navigation to /service-leads/[id]
→ getLeadById() (unstable_cache miss) ← re-fetches from DB
→ Page renders fresh data ✓Adding new domains
To extend this pattern to a new entity:
- Add tag helpers to
apps/web/lib/cache-tags.tsfollowing the naming rules. - Create a
lib/queries/{entity}.tsfile that wraps the servicefindByIdcall inunstable_cachewith the appropriate tags. - Use the query helper in the relevant Server Component instead of calling the service directly.
- Call
revalidateTagwith the entity and list tags in every Server Action that mutates that entity. - Update the call-site table in this document.