Architecture
Stack
| Layer | Technology |
|---|---|
| Dashboard | React + TypeScript, Vite, Redux Toolkit |
| Reverse proxy / static host | Nginx |
| API | Express (Node.js) |
| Database | PostgreSQL |
| Cache / pub-sub state | Redis |
| Realtime transport | Server-Sent Events (SSE) |
| Auth | JWT in an httpOnly cookie |
Request flow
Browser (dashboard) ──▶ Nginx :80 ──▶ / → static React build
└▶ /api/ → backend :3000
SDK client (your app) ──▶ backend :3000 directly
/api/v1/flags (x-sdk-key header)
/api/v1/stream (SSE, sdkKey query param)Nginx (nginx/nginx.conf) serves the built frontend for any non-API path (try_files ... /index.html, so client-side routing works) and proxies everything under /api/ to the backend container, with proxy_buffering off so SSE streams aren't buffered.
Backend routing (backend/app.js)
Routes are mounted in this order:
/api/v1/streamand/api/v1(SDK + SSE) — behindprojectOriginCors, a dynamic CORS middleware (see below). No JWT required — these are keyed by SDK key instead.- Global CORS for the dashboard origins (
localhost:5173/5174in dev) /api/auth— public/api/projects,/api/environments,/api/flags— behindverifyUser(JWT cookie required)
Dynamic per-project CORS
Unlike a typical static CORS allowlist, FlagPulse's SDK/SSE endpoints accept requests from any origin that's been registered as an environment URL. This is implemented with Redis as the source of truth:
- On server start,
syncCorsOrigins()reads every environment URL from Postgres and seeds a Redis set (cors:origin) with their origins. - Creating or editing an environment adds/removes its origin from that same set (
addNewOriginToCache/removeOriginFromCache). - The
projectOriginCorsmiddleware checks incoming request origins againstcors:originviaSISMEMBERon every request. - On shutdown (
SIGINT), the Redis set is cleared and rebuilt fresh on next boot.
Caching strategy
Two things are cached in Redis, both with a 5-minute TTL:
- SDK key → environment ID (
envId@<sdkKey>) — avoids a Postgres lookup on every SDK request. - Environment ID → flag list (
flags@<environmentId>) — the full evaluated flag set for an environment, as JSON.
Both are read-through: a cache miss falls back to Postgres and repopulates the cache. Writes (flag edits, toggles, deletes, SDK key rotation) proactively invalidate or patch the affected cache entries so SDK reads never serve stale data past a single write.
Realtime updates
backend/services/sse.js keeps two in-memory connection maps:
connections: environment ID → list of SDK client response streams (/api/v1/stream)dashboardConnections: environment ID → list of dashboard response streams (/api/v1/stream/dashboard, requires JWT)
When a flag is created, updated, toggled, or deleted, the relevant controller calls sendClient(envId, payload), which writes an SSE data: event to every connected SDK client for that environment. Dashboard connections separately receive presence events with a live count of connected SDK clients whenever one disconnects.
Because connection state is in-memory (not Redis-backed), realtime state does not survive a backend restart or scale beyond a single instance — see Realtime Updates for what this means for SDK clients.