All docs

Realtime: Redis pub/sub → SSE bridge

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 stream

Channels

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.