Real-time sync

Real-time sync

When one family member adds milk to the shopping list on their phone, every other signed-in device — the kitchen tablet, another phone, a laptop in the home office — shows it within a second. No refresh, no pull-to-reload. This page explains how, and what to expect when things go sideways.

The short version

Every authenticated browser or PWA holds a WebSocket open to /ws on the FamilyHub API. When anything interesting happens (a task is completed, a shopping item ticked, a calendar event added, AI categorises an item into a new group), the backend publishes an event on that socket. Each device applies the change to its local state and re-renders — same component tree, different data.

The write path still goes through normal REST calls. The WebSocket only carries notifications that something changed, not the write itself. This keeps the data flow simple: if the WebSocket is down for a moment the app still works, just without the live updates.

What triggers an event

ActionEventWho receives
Add / update / delete a shopping itemSHOPPING_ITEM_UPDATED with the list idEveryone in the family
Check / uncheck a shopping itemSHOPPING_ITEM_UPDATEDEveryone in the family
AI assistant adds/updates itemsSHOPPING_ITEM_UPDATED after the intent executesEveryone in the family
Auto-categorise job reshuffles itemsSHOPPING_LIST_UPDATED on the affected listsEveryone in the family
Create / edit / complete / snooze a taskTASK_UPDATED with the task idEveryone in the family
Daily task-instance generator runsTASK_UPDATED for each newly-created instanceEveryone in the family
Create / edit / delete a calendar eventCALENDAR_EVENT_UPDATEDEveryone in the family
CalDAV-side change synced in from an external clientCALENDAR_EVENT_UPDATEDEveryone in the family
Recipe created or updatedRECIPE_UPDATEDEveryone in the family
AI voice command processedAI_INTENT_EXECUTED with the intent typeJust the submitting device

Events carry an id, not the full payload. The receiving client makes a short REST call for the actual data — that way two devices always end up with the same canonical record, even if the WebSocket delivered events in a slightly different order.

Offline behaviour

Writes you perform while disconnected

Every write is a regular HTTPS call. If you're offline, the call fails and the UI surfaces a retry banner — there's no hidden "pending write" queue. When you come back online, just retry. This is a deliberate trade-off: we avoid the conflict-resolution complexity of a fully offline-capable app, and in return the live state is always canonical.

Events you miss while disconnected

When the WebSocket reconnects, the client doesn't replay missed events — instead it re-fetches the relevant views (the lists you have open, the tasks for today, the calendar events in your current date range). You may briefly see slightly stale data between reconnect and the fetch completing; it'll catch up within a few hundred milliseconds on a decent connection.

How reconnect decides it's time

Each page (shopping, tasks, calendar, dashboard) subscribes to the events it cares about. The underlying WebSocket hook (useWebSocket in familyhub-web) handles:

  • Exponential backoff when the connection drops — starts at 1 s, caps at 30 s
  • Heartbeat every 25 s so proxies don't idle-kill the connection
  • Reconnect on visibility — when you Alt-Tab back to the PWA or wake a suspended tablet, the client eagerly retries if it thinks the socket is stale
  • Token refresh interplay — when the auth cookie is rotated, the WebSocket reconnects with the fresh one

Reverse-proxy setup (self-hosted)

WebSockets are HTTP/1.1 upgrades, so your reverse proxy has to forward Upgrade and Connection headers at /ws. nginx, Traefik, Caddy, HAProxy all support this; the docker-compose setup ships with a nginx config that does the right thing.

If you're seeing the UI silently fail to live-update, the first thing to check is your proxy logs for /ws requests — if they're responding 426 "Upgrade Required" or 101 missing, upgrades aren't being forwarded.

What this is not

  • Not eventual consistency — the canonical state lives in the database. The WebSocket just tells you "go re-read".
  • Not a chat protocol — messages flow server → client only. Commands (ticking an item, finishing a task) go through REST.
  • Not offline-first — reads are live, writes need the server. If you want an offline-capable shopping list for a trip without reception, file a feature request.

Testing it

Open the same family on two devices. On device A, add an item to a shopping list. Device B should show the new item within ~500 ms without any interaction. Tick the item on device B — device A should strike it through instantly. If either side sits stale for more than a second, check the browser's network tab for a /ws connection under "WS".