Key Conventions
Cache key naming patterns, tenant scoping rules, and collision avoidance for the @repo/cache layer.
Key Format
All cache keys follow a colon-delimited hierarchical pattern:
{scope}:{identifier}:{resource}:{qualifier}
Segments
| Segment | Purpose | Example |
|---|---|---|
scope | Isolation boundary (usually tenant) | tenant |
identifier | The scoping ID (org ID, user ID, etc.) | org_abc123 |
resource | What is being cached | settings |
qualifier | Optional sub-resource or parameter | theme, page:3 |
Examples
tenant:{orgId}:settings — Organization settings
tenant:{orgId}:members:count — Member count for an org
tenant:{orgId}:research:{planId}:status — Research plan status
user:{userId}:preferences — User-level preferences
ref:plan-statuses — Reference data (no tenant scope)
Tenant Scoping Rule
Cache keys for tenant-scoped data must include the tenant identifier as the second segment. This is the caching equivalent of the RLS rule: just as every database query uses withTenantContext(), every tenant-scoped cache key includes the org ID.
// Correct — tenant-scoped key
const key = `tenant:${orgId}:settings`;
// Wrong — missing tenant scope risks cross-tenant data leakage
const key = `settings:${settingsId}`;
This convention also enables efficient pattern-based invalidation when a tenant's data changes — you can clear all of a tenant's cache entries with tenant:{orgId}:*.
Global Keys
Some data is not tenant-scoped:
- Reference data — enum values, plan statuses, feature flags. Use the
ref:prefix. - System-wide counters — use the
sys:prefix.
ref:plan-statuses — Cached reference data
sys:health:last-check — System-level operational data
Global keys should be rare. Most application data is tenant-scoped.
Key Length and Characters
- Keep keys under 200 characters. Long keys waste memory and slow lookups.
- Use only lowercase alphanumeric characters, colons, and hyphens. Avoid spaces, special characters, and user-controlled input in key segments.
- If a segment value comes from user input (search queries, etc.), hash it:
import { createHash } from "node:crypto";
function hashSegment(value: string): string {
return createHash("sha256").update(value).digest("hex").slice(0, 16);
}
const key = `tenant:${orgId}:search:${hashSegment(query)}`;
Key Registry
As the number of cached resources grows, maintain a registry of key patterns in the code that produces them. This helps with:
- Discoverability — developers can find all cache keys for a resource.
- Invalidation — knowing the key patterns makes it easy to write correct invalidation logic.
- Debugging — you can scan Redis for keys matching a known pattern.
Currently, key construction is inline at the call site. If the number of distinct key patterns grows beyond a handful, consider extracting a cacheKeys utility:
// Future pattern — not yet implemented
export const cacheKeys = {
tenantSettings: (orgId: string) => `tenant:${orgId}:settings`,
memberCount: (orgId: string) => `tenant:${orgId}:members:count`,
researchStatus: (orgId: string, planId: string) => `tenant:${orgId}:research:${planId}:status`,
} as const;
This is a recommended evolution, not a current requirement.