Stage 7 · Master
Caching & Rate Limiting with Redis
Session Storage in Redis
Refresh-token sessions live in Redis so rotation and revocation are operational changes, not deployment events.
Why Not Self-Contained Refresh Tokens
Fieldwork already uses signed tokens at the HTTP edge, so the first temptation was to make refresh tokens fully self-contained too: sign a long-lived JWT, validate it locally, mint a new access token, done. We rejected that because logout and compromise response become deployment problems instead of data problems. If a refresh token is valid until its signature expires, revoking it means adding blacklist machinery, waiting for caches to converge, or rotating signing material more broadly than the incident requires. None of that is good operational hygiene for a multi-tenant system with real users and sessions on multiple devices.
The opposite extreme — storing every refresh session only in Postgres and hitting the database on every refresh — would have worked, but it puts session churn on the same primary path as task and project data. Refresh is not business data; it is auth control-plane state. That is exactly the kind of short-lived, revocable record Redis is good at. The session needs lookup by opaque ID, TTL expiry, atomic rotation, and cheap revocation. It does not need relational joins.
| Approach | Why it was attractive | Why Fieldwork rejected or chose it |
|---|---|---|
| Long-lived refresh JWTs | No server lookup on refresh | Revocation and reuse detection are clumsy once the token is already out in the world |
| Postgres session table only | Durable and familiar | Pushes volatile auth state onto the same path as product data |
| Redis session store with opaque refresh token | Requires a separate session model | Chosen because TTL, rotation, and revocation are first-class operations |
That is the fundamental design decision. The client gets an opaque token. The authoritative session lives in Redis, where we can expire, rotate, or revoke it without touching deploy pipelines or signing keys.
The Session Shape
Each refresh token maps to a session record keyed by a random session ID. The record stores tenant ID, user ID, a hash of the presented refresh secret, issued-at metadata, expiry, and a small amount of device context for security review. The secret itself is never stored in plaintext. This mirrors password handling more than access-token handling. If Redis were dumped, an attacker should not be able to replay refresh tokens directly from the session values. The token handed to the client is effectively session_id.secret, where the server uses the session ID for lookup and the secret for verification.
- Keep access tokens short-lived and stateless; keep refresh sessions revocable and server-backed.
- Store a hash of the refresh secret, not the secret itself.
- Put tenant identity in the session record so revocation and analytics stay tenant-aware.
Rotation, Revocation, and Reuse Detection
The whole point of this design is that session lifecycle becomes data mutation. On refresh, we verify the secret hash, mint a new access token, create a new refresh session, and delete or mark the old one in the same Redis operation boundary as far as practical. If an already-rotated token shows up again, that is not just an invalid request; it is a reuse signal. That likely means token theft or a buggy client replaying stale credentials. We record that and revoke the user's active sessions for the tenant. That response is much harder to model with self-contained refresh JWTs because you have nothing to point at except signatures and timestamps.
If the only way to force-log-out users is to ship new config or rotate a global secret, the auth model is too blunt. Session revocation should be an operational action backed by data storage.
What Redis Stores and What It Does Not
Redis stores volatile session state and just enough metadata to manage risk: tenant, user, expiry, agent fingerprint, maybe the last rotation time. It does not become the primary user database. Roles, memberships, profile data, and long-lived audit history remain in Postgres. We also do not put access tokens in Redis. Access tokens stay short-lived and self-verifying so ordinary API requests do not require a session lookup. The refresh boundary is where we pay that lookup cost, because that is where lifecycle control actually matters.
| Data | Store | Reason |
|---|---|---|
| Refresh session record | Redis | TTL expiry, rotation, revocation, reuse detection |
| Access token claims | Signed token only | Keep ordinary request auth off the Redis path |
| Memberships and roles | Postgres | Authoritative business data with relational queries |
| Long-term audit trail | Postgres or log pipeline | Durability and reporting requirements |
The Decision
Fieldwork stores refresh-token sessions in Redis as opaque, revocable server-side records. We rejected self-contained long-lived refresh JWTs because they make incident response and logout coarse-grained, and we rejected a Postgres-only session store because session churn does not belong on the same path as core product data. The split is clean: access tokens stay short-lived and stateless for request throughput; refresh sessions live in Redis so rotation, revocation, device-level invalidation, and reuse detection are ordinary data operations. That is the real decision: auth control-plane state should be controllable.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.