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
| Action | Event | Who receives |
|---|---|---|
| Add / update / delete a shopping item | SHOPPING_ITEM_UPDATED with the list id | Everyone in the family |
| Check / uncheck a shopping item | SHOPPING_ITEM_UPDATED | Everyone in the family |
| AI assistant adds/updates items | SHOPPING_ITEM_UPDATED after the intent executes | Everyone in the family |
| Auto-categorise job reshuffles items | SHOPPING_LIST_UPDATED on the affected lists | Everyone in the family |
| Create / edit / complete / snooze a task | TASK_UPDATED with the task id | Everyone in the family |
| Daily task-instance generator runs | TASK_UPDATED for each newly-created instance | Everyone in the family |
| Create / edit / delete a calendar event | CALENDAR_EVENT_UPDATED | Everyone in the family |
| CalDAV-side change synced in from an external client | CALENDAR_EVENT_UPDATED | Everyone in the family |
| Recipe created or updated | RECIPE_UPDATED | Everyone in the family |
| AI voice command processed | AI_INTENT_EXECUTED with the intent type | Just 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".