Socket.IO Backend Setup
What your own backend needs to implement to work with Flux's Socket.IO realtime adapter — a per-channel event bus, a sync handshake for delta catch-up, and a reconciliation handshake, all backed by a single-state changelog.
- An existing Flux app already wired up on the client side, with Socket.IO chosen as its realtime adapter.
- A backend server you control, in any language, with a Socket.IO server implementation (this page shows Node and Python side by side).
- A data source that can tell you when a row changes — DB triggers, change streams, or your own event bus.
- A single-state changelog collection so sync() and reconcile() have authoritative data to query.
Everything on this page is a description of Socket.IO event names and JSON payload shapes — none of it requires Node. Node is shown as the complete worked example because it's what most Flux Socket.IO setups use, but every step includes a Python (python-socketio) equivalent, and the same contract is just as implementable in Go, Ruby, Java, or any other language with a Socket.IO server library. If your backend already runs in one of those, translate the event names and payload shapes directly — nothing about the adapter contract assumes a JavaScript runtime on the other end.
The contract you're building against
channel field carried inside the payload — the Socket.IO event name IS the routing mechanism. The adapter attaches a listener for the exact string `${channel}:${event}`, and your server emits under that same string:Because Socket.IO already dispatches by event name to every connected socket, you don't need to track which sockets are "subscribed" to which channel. Broadcasting a change is just one io.emit() call — only clients listening for that exact event name act on it. This is the one place Socket.IO is simpler than the raw WebSocket or SSE transports.
Install your server dependencies
npm install express socket.io cors dotenv mongoose. Python: pip install python-socketio fastapi "uvicorn[standard]" motor. Either way, add whatever driver your data source needs — none of that is Socket.IO-specific.The sync anchor handshake (secondary signal)
FLUX_SYNC_ANCHOR carrying the full SyncAnchorMap. This is a bulk session signal, not the primary catch-up mechanism. The per-channel authoritative handshake is flux_sync_request (Step 4).FLUX_SYNC_ANCHOR fires once per connection with the full anchor map — useful for server-side logging or session tracking. flux_sync_request fires once per channel subscription with the specific anchor for that channel — this is what actually drives row-level backfill. Both are kept consistent: neither is gated on anchor being non-zero.
The authoritative handshake: flux_sync_request / flux_sync_complete
flux_sync_request with { channel, last_sync_anchor } and waits for a matching ${channel}:flux_sync_complete response. This is what gates whether Flux refreshes its local cache clock.The client-side adapter reads payload.rows and forwards each one through normalizeDeltaRow() before resolving. A response with an empty or missing rows array resolves successfully but delivers zero data — the client will warn about this in the console.
Reconciliation: confirming deletions the delta log missed
flux_sync_complete can report. reconcile() is the safety net — the client sends its full list of locally-cached IDs for a channel, and your server confirms which of them no longer exist.Socket.IO event handlers
FLUX_SYNC_ANCHOR, flux_sync_request, and flux_reconcile_request — are registered inside a single connection handler:The changelog: single-state delta storage
op: 'DELETE' event, which a standard query on a live database table cannot accomplish.Query helpers: backfill and deletion check
queryChangesSince) and the reconciliation handshake (queryExistingIds).Wiring your data source to broadcast and log
logChange() so sync and reconciliation have state, and call io.emit() to push live frames to connected clients.Mounting the server
cors option lives on the SocketIOServer constructor — the Express cors() middleware does not cover the Engine.IO handshake.Client store adapters: consuming the wire format
setState handles both full array payloads (from initial bootstrap) and single delta objects (from live events and sync complete).Testing your endpoint
npm install --save-dev socket.io-client. Python: pip install "python-socketio[client]".items:flux_sync_complete response after emitting flux_sync_request, then trigger a database mutation — you should see an items:UPDATE event print immediately.Horizontal scaling — a single server process can only broadcast to sockets connected to that exact process. Running more than one instance behind a load balancer requires a shared adapter — @socket.io/redis-adapter on Node, or python-socketio's own Redis/Kafka AsyncManager on Python — so a change detected on instance A is relayed to a client connected to instance B.
Sticky sessions — if you haven't adopted a shared adapter, Socket.IO's long-polling fallback transport requires all requests from the same client to land on the same server process; configure sticky sessions at your load balancer or force transports: ['websocket'] client-side to sidestep this entirely.
Auth on the handshake — Socket.IO clients CAN attach an auth token via the auth option at connection time. Verify it in a connection middleware before accepting the socket (Node: io.use(); Python: python-socketio's connect handler can reject by raising ConnectionRefusedError), rather than trusting every incoming connection unconditionally.
Running alongside raw WebSocket — if you also run a raw ws server on the same HTTP port, initialize it with new WebSocketServer({ noServer: true }) and route upgrade events manually in server.on('upgrade'), passing /socket.io/ paths through untouched. Otherwise Engine.IO and ws fight over the upgrade handshake.
Reverse proxy WebSocket upgrade — behind Nginx, the location block proxying to your Socket.IO server needs proxy_set_header Upgrade $http_upgrade; and proxy_set_header Connection "upgrade";, or connections silently fall back to (slower, chattier) long-polling.
On the client side, this is the server the Socket.IO tab in getting-started/react and getting-started/nextjs connects to — nothing on this page needs to know anything about Zustand, Redux, Jotai, or Stunk; the state-manager choice lives entirely on the client. For the deeper explanation of sync anchors and delta catch-up across every transport (not just Socket.IO), see core-concepts/sync-anchors.