Redis Keys¶
Inventory of every Redis key pattern the backend reads or writes. Keys are grouped by domain. Patterns use {var} for runtime-substituted values.
This list is reconstructed by auditing backend/internal/platform/cache/cache.go (the Redis abstraction) and every caller across the domain packages under backend/internal/services/. If you add a new key, add it here too, since drift makes incident response painful.
Asynq queues
The background-task queues used by hibiken/asynq (asynq:* keys, including queue lists, scheduled sets, processing zsets, and unique-task locks) are managed internally by the library. They are not enumerated below — treat them as opaque. The library's own dashboard tools (asynq stats) are the right inspection path.
Authentication & sessions¶
| Key Pattern | Type | TTL | Purpose | Set By | Read By |
|---|---|---|---|---|---|
refresh_token:{token} |
STRING | refresh token expiry | Maps a refresh-token string to its owning user_id. Deleted on logout. |
services/auth/session_service.go |
services/auth/session_service.go |
register_device:{fingerprint} |
STRING | 30 days | Count of registrations from a device fingerprint. Throttles bulk-account abuse. | services/auth/auth_service.go |
services/auth/auth_service.go |
webauthn:reg:{user_id} |
STRING (JSON) | 5 min | WebAuthn registration session data (challenge + options). | services/auth/auth_handler.go |
services/auth/auth_handler.go |
webauthn:login:{user_id} |
STRING (JSON) | 5 min | WebAuthn login session data (challenge + options). | services/auth/auth_handler.go |
services/auth/auth_handler.go |
apple_public_keys |
STRING (JSON) | 24 h | Cached Apple JWKS used to verify Sign-in-with-Apple identity tokens. | services/auth/auth_service.go |
services/auth/auth_service.go |
user:privacy:{user_id} |
STRING ("0"/"1") |
24 h | Cached is_location_shared flag, avoids a DB hit on every location update. Invalidated when user toggles sharing. |
services/friend/friend_service.go |
services/friend/friend_service.go, services/auth/auth_service.go (invalidate) |
Rate limiting & abuse¶
Most rate-limit keys are written by the Lua INCR + EXPIRE script in platform/cache/cache.go::RateLimit, keyed as {prefix}:{identifier} where identifier is the client IP (for the middleware presets) or a user id (for enrich:*). item:rl:* is the exception: a plain GetCache/SetCache counter rather than the atomic Lua limiter.
| Key Pattern | Type | TTL | Purpose | Set By | Read By |
|---|---|---|---|---|---|
register_ip:{ip} |
INT | 1 h | Registration attempts per IP (default limit: 5/h). | middleware/rate_limiter.go |
middleware/rate_limiter.go |
login_ip:{ip} |
INT | 15 min | Login attempts per IP (default limit: 10/15 min). | middleware/rate_limiter.go |
middleware/rate_limiter.go |
api_ip:{ip} |
INT | 1 min | Baseline API requests per IP on /api/v1/* (limit: 600/min). |
middleware/rate_limiter.go |
middleware/rate_limiter.go |
reset_password_ip:{ip} |
INT | 1 h | Password-reset token attempts per IP (limit: 5/h, prevents brute force). | middleware/rate_limiter.go |
middleware/rate_limiter.go |
item:rl:{user_id}:{minute} |
INT | 2 min | Per-user capture-item throttle (limit: 60/min). Windowed by unix-minute suffix; a plain INCR-style counter, not the Lua limiter. |
services/plan/item_service.go |
services/plan/item_service.go |
enrich:parse:{user_id} |
INT | 24 h | Per-user/day budget for billable link-enrichment passes (CAPTURE_DAILY_PARSE_LIMIT, default 50). Dev/admin accounts bypass. |
services/plan/item_service.go |
services/plan/item_service.go |
enrich:upgrade:google |
INT | 24 h | Global daily budget for upgrading Serper-minted google_places rows via the billed Places details API (limit 200/day). |
services/location/location_service.go |
services/location/location_service.go |
blocked_ip:{ip} |
STRING (blocked:{reason}) |
configurable | Hard-blocked IPs (set by IP-blocker middleware). | middleware/ip_blocker.go |
middleware/ip_blocker.go |
violations:{ip} |
INT | configurable | Cumulative violation counter, a graduated response before hard-blocking. | middleware/ip_blocker.go |
middleware/ip_blocker.go |
Presence & active location¶
| Key Pattern | Type | TTL | Purpose | Set By | Read By |
|---|---|---|---|---|---|
presence:{user_id} |
STRING ("1") |
3 min | Heartbeat marker, user is online if key exists. Refreshed by client ping. | services/presence/presence_service.go |
services/chat/service.go, services/friend/friend_service.go (via MGet) |
active_location:{user_id} |
STRING (JSON) | configurable | User's currently-active shared location (the live pin). Deleted when user toggles off. | services/presence/presence_service.go |
services/presence/presence_service.go, services/friend/friend_service.go (via MGet) |
user:location:{user_id} |
STRING (JSON: {lat, lng, updated_at, arrived_at}) |
24 h | Latest location + stay-duration tracking. Backbone of nearby-friends discovery. | services/friend/friend_service.go |
services/friend/friend_service.go, services/discovery/service.go, sim workers |
Chat & messaging¶
| Key Pattern | Type | TTL | Purpose | Set By | Read By |
|---|---|---|---|---|---|
chat:messages:{chat_id} |
LIST | 24 h | Cached recent messages for a chat (trimmed to 100). Read-through on hit, DB fallback on miss. | services/chat/service.go |
services/chat/service.go |
chat:online:{event_id}:{user_id} |
STRING | 1 h | Marker that {user_id} is currently active in event {event_id}. Deleted on offline. |
services/chat/service.go |
services/chat/service.go |
chat:event:{event_id} |
Pub/Sub channel | n/a | Cross-pod chat fanout channel. Envelope {opid, xuid?, p}: opid is the publishing pod's UUID (subscriber drops own echoes), xuid skips a UserID on receive, p is the raw WS payload. |
SessionHub.publishRemote (internal/platform/ws/session_hub.go) |
SessionHub.runSubscriber (PSUBSCRIBE chat:event:* on every pod) |
Friends¶
| Key Pattern | Type | TTL | Purpose | Set By | Read By |
|---|---|---|---|---|---|
user:friends:{user_id} |
STRING (JSON) | friendListCacheTTL (10 min) |
Cached accepted-friend list for a user. Invalidated on accept/remove. | services/friend/friend_service.go |
services/friend/friend_service.go |
Geo discovery¶
| Key Pattern | Type | TTL | Purpose | Set By | Read By |
|---|---|---|---|---|---|
events:geo |
GEO (ZSET) | none | Geospatial index of all event locations. Backs GetNearbyEventIDs via GEORADIUS. |
platform/cache/cache.go::AddEventLocation(s) |
platform/cache/cache.go::GetNearbyEventIDs |
Spatial data
Location coordinates live in Postgres (PostGIS geometry + denormalized lat/lng columns), not Redis. Redis only holds the raw lat/lng geo set above; viewport clustering happens in SQL inside services/discovery/service.go.
Utility caches¶
| Key Pattern | Type | TTL | Purpose | Set By | Read By |
|---|---|---|---|---|---|
link-preview:{sha256(url)} |
STRING (JSON) | 1 h | OpenGraph metadata cache for outbound URL previews. Keyed by SHA-256 hex of the URL. | services/media/link_preview_handler.go |
services/media/link_preview_handler.go |
enrich:place:{location_id} |
STRING (JSON) | 14 d | A place's curated enrichment (discovered links, CID/FID, thumbnail) reused by later captures of the same place without a Serper spend. | services/plan/item_service.go |
services/plan/item_service.go |
enrich:item:lock:{item_id} |
STRING ("1") |
5 min | Per-item work lock (SETNX) so a double-fired link:enrich task can't double-spend the billable pass. Released on completion. |
services/plan/item_service.go |
services/plan/item_service.go |
semantic:extract:{hash} / semantic:select:{hash} |
STRING (JSON) | 14 d | Memoized positive LLM answers, keyed by the first 16 bytes of the SHA-256 of the rendered prompt (hex); identical re-captures skip the model calls. | platform/llm/semantic_resolver.go |
platform/llm/semantic_resolver.go |
loc:area:{normalized_name} |
STRING (JSON, - = negative) |
1 h | Gazetteer area lookup by NFKC-folded name, so repeated area-scoped searches skip the DB; - caches a miss. |
services/location/location_area.go |
services/location/location_area.go |
loc:qvec:{normalized_query} |
STRING (vector) | 1 h | Memoized query embedding for semantic location search, so repeat queries skip the embeddings call. | services/location/location_embedding.go |
services/location/location_embedding.go |
Async (Asynq)¶
Background work — both event-driven tasks and cron-driven tasks — runs through Asynq. All of Asynq's Redis keys (asynq:*, including queue lists, scheduled sets, processing zsets, archived/dead-letter, and asynq.Unique locks) are managed internally by the library and treated as opaque per the note at the top of this doc. See backend/infrastructure/async.md for the queue and scheduler model.
Cleanup keys¶
Used by the test-data seeder (cmd/test/data/main.go) to wipe state between simulation runs:
presence:*
user:location:*
active_location:*
user:friends:*
user:privacy:*
If you add a new user-scoped Redis key, add its prefix to that cleanup list too — otherwise stale data leaks between test runs.
Adding a new key¶
- Define the key shape in the service that owns it. Prefer
domain:purpose:{id}(lowercase, colon-separated). - Document it in the table above with type, TTL, purpose, writers, readers.
- If it's user-scoped and ephemeral, add the prefix to the simulation cleanup list.
- If it's a rate-limit key, route it through
middleware/rate_limiter.goso the Lua atomic INCR script is used.