Redis key inventory
Single reference for every Redis key the stack writes (TASK-0127 added the
API-key rows; the rest pre-date it). Key formats use :name for the
interpolated value. Where a TTL is fixed it is given; "window" keys expire
with the sliding window (PEXPIRE), and BullMQ queue keys are managed by
the queue (see the per-queue notes).
All application keys live in logical DB 0 of the single Redis instance
(REDIS_URL). The realtime gateway and BullMQ use the same instance; their
keys are namespaced so they never collide.
Authentication & API keys
| Key | Type | TTL | Set by | Purpose |
|---|---|---|---|---|
apikey:authfail:{apiKeyId} |
string 1 |
1 h | auth.guard.ts |
Throttle api_key.auth_failure audit writes (one per hour per key, SET … EX 3600 NX) so a revoked/expired key hammered by a client cannot flood the audit table. Fail-open: Redis down → the audit is still written. |
rl:api:{subject} |
zset | 60 s window | rate-limit.guard.ts |
Sliding-window rate-limit bucket. subject is the API key id for fk_ credentials (per-key window), the user id for JWT callers, the client IP for anonymous/public routes. Limit: key's rateLimit → RATE_LIMIT_API_KEY (600, docs §11) for keys; RATE_LIMIT_API (100) otherwise. |
rl:auth:{subject} |
zset | 60 s window | rate-limit.guard.ts |
The auth-class bucket (/api/v1/auth/* — login/register/refresh/reset), RATE_LIMIT_AUTH (10/min) to keep credential stuffing linear. |
Idempotency (24 h memo)
| Key | Type | TTL | Set by | Purpose |
|---|---|---|---|---|
idem:{userId}:{idemKey} |
string (JSON) | 24 h | middleware/idempotency.ts |
Memo of a user's idempotent mutation (Idempotency-Key header). A HIT replays the stored 2xx body without re-running the handler. |
idem:api:{workspaceId}:{apiKeyId}:{digest}:{idemKey} |
string (JSON) | 24 h | middleware/idempotency.ts |
The API-key keyspace (TASK-0127): scoped by credential + route + body digest (digest = first 32 hex of sha256(METHOD + " " + path + JSON(body))), so a same-key/different-body call is a different operation and no cross-key/cross-workspace replay is possible. |
ai:generate:idem:{workspaceId}:{userId}:{idemKey} |
string (JSON) | 24 h | ai/generator/generate.service.ts |
The AI-generate endpoint's IN-BAND idempotency (it streams/SSE — the body interceptor opts out via @SkipIdempotency() and re-emits the terminal frame on a key HIT instead; the created resource is not re-created). |
Search & recent
| Key | Type | TTL | Set by | Purpose |
|---|---|---|---|---|
search:recent:{subject} |
list (capped 20) | none (pruned) | search/search.service.ts |
Recent-searches history. subject is the user id, or api:{apiKeyId} for API-key traffic (keys get their own history; the creator's list is never polluted). |
search:recent:rl:{userId} |
zset | 60 s window | search/search.service.ts |
Recent-searches write throttle. |
search:reconcile:last |
string (ISO) | 30 d | worker search-index/processor.ts |
Last successful reconcile timestamp (org-admin Search-health tab, T-177). |
AI budgets & per-user limits
| Key | Type | TTL | Set by | Purpose |
|---|---|---|---|---|
ai:calls:rl:{userId} |
zset | 60 s window | ai/ai-rate-limit.ts |
Per-user AI call budget (30 calls / 60 s) across the AI surface. |
ai:rl:ws:{workspaceId}:{minute} |
string counter | until minute rollover | ai/gateway-provider.ts |
Per-workspace per-minute AI gateway counter (org-level throttle). |
ai:budget:notified:{workspaceId} |
string | 1 h | ai/gateway-provider.ts |
Once-per-hour "budget exhausted" notification dedup. |
djb2:{h} |
string | none | ai/database-ai.service.ts |
Deterministic hash cache for database-AI outputs. |
writing:{action}:{lang} |
string | — | ai/ai-writing.service.ts |
Writing-context action/language memo. |
Forms (public)
| Key | Type | TTL | Set by | Purpose |
|---|---|---|---|---|
form:rl:{slug}:{ip} |
zset | 60 s window | forms/rate-limit.ts |
Per-IP submit rate limit for a public form (abuse control on the unauthenticated surface). |
questions:{formId} |
string (JSON) | — | forms/forms.service.ts |
Hot cache of a form's question set. |
Files & uploads
| Key | Type | TTL | Set by | Purpose |
|---|---|---|---|---|
file:{fileId}:step:{name} |
string | per step | worker processors/files/processor.ts |
Step markers for the file-processing pipeline (resumability). |
Automation & worker
| Key | Type | TTL | Set by | Purpose |
|---|---|---|---|---|
auto:webhook-rate:{webhookId} |
zset | window | worker automation.ts |
Per-incoming-webhook trigger rate limit. |
automations:matched:{triggerType} |
set | — | worker automations/matcher.ts |
Trigger-type → automation index. |
automations:run:{automationId} |
string | — | worker automations/schedule.ts |
Scheduler lock/marker per automation. |
automations:runs:{status} |
zset | — | worker automations/pipeline.ts |
Run queue by status. |
automations:dead_letter:{workspaceId} |
list | — | worker automations/dead-letter.ts |
Dead-letter runs (retryable after inspection). |
automations:actions:{type}:succeeded / :failed |
string counters | — | worker automations/pipeline.ts |
Action success/failure counters (observability). |
worker:heartbeat:{WORKER_ID} |
string | 30 s | worker index.ts |
Liveness heartbeat (the watchdog treats a missing heartbeat as "worker down"). |
worker:dlq-reported:{deliveryId} |
string | 24 h | worker maintenance.processor.ts |
"DLQ already reported" dedup (one alert per dead item). |
worker:retention:report |
string | — | worker maintenance.processor.ts |
Retention-job last-run marker. |
lock:{blockId} / lock:{refId} |
string (SETNX) | 30 s | worker search-index/processor.ts |
Single-flight locks for search-index rebuilds (one worker per doc at a time). |
page:{pageId}, block:{id}, database:{id}, record:{id}, file:{id}, comment:{id} |
JSON | — | worker search-index/builders.ts |
Search-document build cache (the OpenSearch doc, memoized per source). |
Notifications & email
| Key | Type | TTL | Set by | Purpose |
|---|---|---|---|---|
notified:{workspaceId} |
string | 1 h | notifications | Digest-notification dedup per workspace. |
Not a Redis key (kept here for completeness)
-
Instant-email dedup — the "email already sent for this notification" guard is the BullMQ job key
email_{notificationId}(on theemailqueue — BullMQ dedups identical jobIds) + theNotification.emailSentAtflag (checked at decision time AND before a re-send). It is NOT a standalone Redis key. The key is colon-free because BullMQ 5.x rejects custom jobIds containing:unless exactly 3-segment (the oldemail:{id}threw on every enqueue — fixed 2026-10-12, see FEATURE_TRACKER T-176/177/178). -
BullMQ queues (
webhooks,search-index,files,automations,ai,email,maintenance) — BullMQ manages its ownbull:<queue>:*keys (wait/active/delayed/prioritized sets, job hashes, event streams). They are not application keys; the queue name is the only stable part. -
Refresh tokens / sessions — stored in Postgres (
RefreshToken,Session), not Redis; revocation is immediate by row delete. -
API key material — the raw
fk_…key is never stored (only itssha256hash,apiKey.keyHash, in Postgres); the last-used bump is a Postgres column debounced to ≤ 1 write / 60 s.
Conventions
- Keys are namespaced by subsystem (
rl:,idem:,ai:,search:,form:,worker:,automations:) soSCAN-based ops can reason per subsystem. - TTLs are explicit at the write site (no fire-and-forget keys except the worker heartbeat, which is short-lived by design).
- Fail-open is the degradation policy for the rate limiters and the idempotency memo (availability over strictness); a Redis outage degrades to "execute, don't store" / "don't throttle" and logs at most once per minute.
- Secrets are never in a key name (key ids / hashes only), and a key's value is never a credential — idempotency values are the stored response body of an already-authenticated call.