Adapters

Socket.IO

Hand Flux an existing socket.io-client instance and it takes care of turning your own server's events into live store updates — reconnect handling, single-socket coordination across tabs, and catch-up after time offline all come for free once the adapter is wired in.

Prerequisites
  • An existing backend emitting events over Socket.IO — any language, any framework
  • socket.io-client already installed on the frontend
  • @tsworldtech/flux and @tsworldtech/flux-socketio installed
1

Create the adapter

Create a socket.io-client instance yourself, then pass it into createSocketIOAdapter(). Set autoConnect: false when constructing the socket — Flux opens the connection itself once a store actually registers, rather than connecting immediately on page load regardless of whether anything needs it.
typescript
1import { io } from 'socket.io-client'
2import { createFlux } from '@tsworldtech/flux'
3import { createSocketIOAdapter } from '@tsworldtech/flux-socketio'
4
5const socket = io(process.env.NEXT_PUBLIC_SOCKET_URL!, {
6 autoConnect: false, // let Flux control when the connection opens
7})
8
9const flux = createFlux({
10 adapter: createSocketIOAdapter(socket),
11})
2

Register a store

channel and event together form the exact socket event name Flux listens for. Whatever name you register is the name your server emits under for live forward events.
typescript
1flux.register({
2 store: useItemsStore,
3 channel: 'items',
4 event: 'UPDATE',
5 idbKey: 'items:all',
6 ttl: 'medium',
7 ingestionType: 'COLLECTION_ALL',
8 diffBeforeUpdate: true,
9 hydrateState: (store, data) => {
10 // store.getState().setState handles both array hydrations and single delta frames
11 store.getState().setState(data)
12 },
13})
14
15// This registration listens for the live socket event named 'items:UPDATE' —
16// your server needs to emit under that exact string. See below.
3

The channel:event naming convention

This is the one contract that has to match between client and server. A registration's channel and event combine into a single string, `${channel}:${event}`, and that string is exactly what your server emits under for live events.
typescript
1// channel + event combine into one socket event name: "channel:event"
2flux.register({ channel: 'orders', event: 'CREATE', /* ... */ })
3flux.register({ channel: 'orders', event: 'UPDATE', /* ... */ })
4flux.register({ channel: 'jobs', event: '*', /* ... */ }) // all ops
5
6// Your server side emits must match these strings exactly:
7// io.emit('orders:CREATE', event)
8// io.emit('orders:UPDATE', event)
9// io.emit('jobs:*', event) // or emit under the specific op that occurred
No per-channel subscribe handshake required for live events

There's nothing to "subscribe" to on the server beyond emitting under the right event name — your backend can broadcast live updates with one plain io.emit() call, and only clients that registered a listener for that exact name react to it.

4

Protocol Handshakes: Sync and Reconcile

In addition to live `${channel}:${event}` broadcasts, the Socket.IO adapter executes explicit message handshakes for delta catch-up and deletion reconciliation:
typescript
1// ── 1. Secondary Bulk Signal (Emitted by adapter on connect) ─────────────
2// Emits full anchor map on connect: { [channel]: unixMs }
3socket.emit('FLUX_SYNC_ANCHOR', { items: 1718000000000 })
4
5// ── 2. Primary Per-Channel Catch-up Handshake ─────────────────────────────
6// Adapter requests deltas for subscribed channel:
7socket.emit('flux_sync_request', { channel: 'items', last_sync_anchor: 1718000000000 })
8
9// Server responds on channel-scoped event:
10socket.emit('items:flux_sync_complete', {
11 rows: [
12 { op: 'UPDATE', data: { id: '662f...', price: 42 }, timestamp: 1718000001234 }
13 ]
14})
15
16// ── 3. Deletion Reconciliation Handshake ───────────────────────────
17// Adapter verifies cached local IDs against database state:
18socket.emit('flux_reconcile_request', { channel: 'items', localIds: ['id1', 'id2'] })
19
20// Server returns IDs that no longer exist:
21socket.emit('items:flux_reconcile_complete', { deletedIds: ['id2'] })
5

What your server needs to send

Every live event your backend emits needs to match a normalized shape:
typescript
1// Every frame your server emits must carry this shape:
2{
3 entity: string // e.g. 'item', 'order', 'job'
4 id: string // the row's stable identifier
5 op: 'CREATE' | 'UPDATE' | 'DELETE'
6 data: any // full row or sparse diff (or id-only shape for deletes)
7 timestamp: number // your server's Date.now() at send time
8 source: string // any string identifying your server
9}
Any server running a Socket.IO implementation that emits under the right event name works here. A worked Node.js example lives in getting-started/node-socketio, but nothing about the adapter itself is Node-specific.
6

Authentication

Auth is handled by the socket instance, not the adapter

Pass an auth token the same way you would with any socket.io-client connection — via the auth option at construction, or updated before a reconnect via socket.auth. Flux never inspects or modifies this; it only calls connect() and disconnect() on the socket instance you give it.

7

Multiple open tabs

One connection per device, not per tab

Only one browser tab holds the live connection at a time — if you open the same app in five tabs, your server still only sees one socket for that device. The other tabs stay fully in sync without opening a connection of their own, and if the tab holding the connection closes, another tab takes over automatically.

Everything else — reconnecting after a dropped connection and catching back up on whatever changed while you were offline — happens automatically once the naming convention and handshake handlers above are implemented correctly. There's nothing further to configure on the client for a standard setup.

For the server-side implementation this adapter expects, see getting-started/node-socketio. For what happens during reconnect specifically, see core-concepts/sync-anchors and core-concepts/activity-bus.