# Multi-instance architecture

The router sends **execution** to the service that owns a route; Socket.io plus shared Redis provide **delivery** to sockets attached to other instances. Those are different jobs. The browser has one socket for rooms, presence and callbacks even if typed API/sync invocation is routed over HTTP/SSE. Redis fan-out cannot execute a handler that was never bundled into a service. See [packaging](./ARCHITECTURE_PACKAGING.md) and [sync](./ARCHITECTURE_SYNC.md).

## Deployment invariants

- Split topology is opt-in; keep service ownership, deployed bindings and invocation mode aligned. In socket invocation mode a handler absent from the socket-owning backend cannot run; routed invocation requires a reachable owning service. A custom HTTP route also needs explicit ownership because its handler predicate cannot be inferred by the router.
- All instances that must share a room must use the **same Redis**. Different healthy Redis endpoints cause silent cross-instance message loss. Use the boot handshake and a real cross-instance delivery check; a green health endpoint alone does not prove shared fan-out.
- The opposite mistake is just as silent: **two environments on one Redis server**, such as a dev laptop tunnelled into staging Redis, or staging and production on one managed instance. The Socket.io adapter's channel key is the cluster boundary. If two environments share a key, they become one cluster. A staging broadcast then reaches a developer's browser, and staging's cross-instance recipient lookup waits on a sleeping laptop until the adapter times out. The framework derives the adapter key from the project name plus the deploy environment, so environments are separated without a setting to forget; see `packages/core/src/socketRedisAdapter.ts` and ADR 0063. That key separates **only the socket cluster**. Sessions and other Redis keys are namespaced by project name alone, so environments that share a Redis server also need distinct project names or separate Redis servers to keep their session stores apart.
- Socket.io's per-instance maps (adapter rooms/sids, the local socket list) look complete but only know this process's sockets. Code built on them works on one server and silently misses members on two. Use the cross-instance room lookup `getRoomSockets()` from `@luckystack/core`, or `fetchSockets()`. The `luckystack/no-local-socket-enumeration` lint rule catches written access. Outside production, a guard on the shared server instance catches computed access, with an explicit raw opt-in for deliberate per-instance work. The guard is development-only because throwing on a hot production path would be worse than missing a recipient. See ADR 0061.
- Keep shared session/cookie secrets and resource identities aligned across instances; otherwise a user can appear logged out on the next backend. Treat a Redis-backed best-effort lease separately from a transactional exactly-once guarantee.
- The router binds a service destination, not an automatic fleet balancer. Scaling the socket-owning service across machines requires an appropriate load balancer and a shared Redis adapter. Redis allows broadcasts and per-recipient delivery across those instances; it does not make all routes available everywhere.
- Run the router on Node while Bun's `node:http` WebSocket-upgrade support remains broken; otherwise HTTP can look healthy while sockets fail. Backends can use a different runtime. See `packages/router/src/` for the current boot-time guard.
- Production safety follows production configuration, not an environment *name* chosen for a topology. Development conveniences that target a particular backend are not production routing guarantees.

## Verification that proves the boundary

Use a Redis-required integration run rather than treating a skipped Redis suite as a pass. Check a real client through the router and another backend's room fan-out; test both regular sync delivery and streamed broadcasts when relying on both. `npm run smoke:ws` and the Redis-required integration tests are documented in [hosting](./HOSTING.md) and package tests. Do not run them against shared production state without authorization.
