flowpanel ships **two SSE drivers** out of the box:

- **memory** (default) — events live in the running Node process. Perfect for
  `next dev`, single-instance previews, single-container deployments.
- **redis** — events fan out via Redis pub/sub. Required the moment you scale
  past one Next.js instance.

## When you need Redis

The browser opens an `EventSource` connection to **one** instance. If a
mutation lands on a different instance, the in-memory `publishResource()`
fires on the wrong process and the subscribed browser never hears about it.

**Symptoms:** Realtime works locally but not in production. Lists don't
refresh after a peer's edit. The dashboard's "Orders scraped today"
counter doesn't tick up.

You need Redis any time **all** of these are true:

- More than one Next.js instance answers requests for the same admin
  (Coolify with replicas > 1, Vercel with multiple regions, Fly with
  multi-region, Render with autoscale, etc.).
- Sticky sessions are not configured (sticky sessions partially work
  around the issue but break under instance restarts).
- Users have the admin open in browsers and you want their lists to
  update when a peer writes.

If you're just running `next start` on one box, the memory driver is fine.

## Switch on Redis

```ts
// flowpanel.config.ts
import { defineAdmin } from "@flowpanel/kit";

export default defineAdmin({
  // …adapter, auth, resources…
  realtime: {
    driver: "redis",
    url: process.env.REDIS_URL!,
    // Optional: prefix every channel so multiple admins on the same Redis
    // instance don't collide. Defaults to "flowpanel".
    keyPrefix: "fp",
  },
});
```

That's the whole switch. Every existing `resource.realtime: true`,
`table({ realtime: true })`, and `publishResource(name, …)` call now
fans out via Redis instead of an in-process EventEmitter.

## What flows over Redis

flowpanel never publishes row contents over Redis. Channels carry just
**resource name + action**:

```
fp:resource:orders → { action: "update", id: "ord_…" }
fp:resource:users  → { action: "create", id: "usr_…" }
fp:custom:scraper-pipeline → arbitrary user payload via ctx.publish
```

Browsers subscribed to a list get the channel ping, then
`router.refresh()` re-fetches the (RSC-streamed) updated rows over
HTTPS — Redis is the **notify**, not the source of truth.

## Operational checklist

- **Connection pooling.** ioredis defaults are fine on small admins
  (under 1k concurrent SSE clients). At scale, share one publisher instance
  across the whole Next.js process — don't recreate per request.
- **TLS.** Production Redis providers (Upstash, AWS ElastiCache, Render)
  use TLS. Pass `tls: {}` (object, not `true`) to ioredis options if your
  URL doesn't already encode it as `rediss://…`.
- **HEAD requests / health checks.** SSE responses are long-lived;
  ALBs default to 60s idle timeout. Bump the proxy/LB idle timeout to
  at least 5 minutes, or accept that browsers auto-reconnect every
  minute (works but spammy in logs).
- **Restart hygiene.** Each Next.js instance opens one Redis subscriber
  connection. When you redeploy, in-flight EventSources disconnect and
  reconnect automatically — no data loss, but expect a thundering herd
  of reconnects right after rollout. Stagger your rolling deploy if you
  see Redis CPU spikes.

## Troubleshooting

**Symptom: Browser opens SSE, server sends events, browser receives nothing.**

The proxy/CDN is buffering. SSE needs the response body to stream as
each event lands; common breakage:

- nginx: set `proxy_buffering off;` for the `/api/flowpanel/stream` location.
- Cloudflare: enable "Streaming responses" or disable proxying on the
  stream route via a Page Rule.
- Vercel: free tier closes Functions at 10s; stream needs `runtime:
  "nodejs"` + Pro tier (or self-host the stream route on a long-lived
  server).

**Symptom: Browser receives events on instance A but not after a
deploy that lands on instance B.**

Almost always a publisher misconfiguration. The mutation route on
instance A is fanning out in-memory only because either:

1. `realtime` is unset (or `{ driver: "memory" }`) in `defineAdmin` — it
   defaults to the in-memory driver, which can't cross instances, OR
2. `realtime.url` points at a Redis that instance B can't reach, OR
3. `REDIS_URL` env was set on A but not on B during a partial rollout.

`flowpanel doctor` (1.0.x) reports the active publisher driver and
catches case 1 and 2 — case 3 needs a CI gate on env parity.
