Realtime: Redis pub/sub → SSE bridge
Route: apps/web/app/routes/sse.tsx Endpoint: GET /sse?org={uuid} Transport: Server-Sent Events (text/event-stream).
Why SSE, not WebSocket
The browser only consumes events — clients post writes through MCP, not over this socket. SSE is one-directional, native (EventSource), and goes through nginx with proxy_buffering off + X-Accel-Buffering: no without the Upgrade: websocket dance.
Connection lifecycle
client EventSource(/sse?org=…)
│
│ loader() subscribes a fresh ioredis client to
│ - pubsub:org:{orgId}:tasks
│ - pubsub:org:{orgId}:plans
│
├─ "event: open\ndata: {orgId, channels}" ← handshake
│
├─ each Redis message → "data: <json>\n\n"
│
├─ every 15 s → ": ping\n\n" (nginx idle timeout is 120 s)
│
▼ request.signal.aborted → unsubscribe + quit + close streamChannels
The handshake hard-codes tasks + plans for the requested org. Per-user notification channels (pubsub:org:{orgId}:notifications:{userId}) land when the Remix session middleware ships in task 35 — we don't subscribe to them today because the loader can't see the user.
Auth (today vs task 35)
Production traffic is rejected with 401 until task 35 wires the real session check. NODE_ENV=development and DEV_AUTH_BYPASS=true short- circuit the gate so this endpoint is usable in dev/test.
Idempotence / replay
There is no replay buffer. Browsers reconnect automatically (~3 s default) and we tolerate losing a couple of events between reconnects — full revalidation via Remix useRevalidator covers the gap. If UX feedback demands it, the channel surface can move from PUBSUB to XADD streams with Last-Event-ID resume; the URL shape stays the same.