set-up-realtime
Use when adding live server-push updates to a frontend SPA - a WebSocket seam with backoff reconnect that writes validated pushed data into the TanStack Query cache and exposes connection status as UI
Files3
View on GitHubSKILL.mdskills/frontend/set-up-realtime/SKILL.md
GitHubSet Up Realtime
Live server→client updates, written into the Query cache through the same server/UI boundary the rest of the app uses. A realtime message carries server data, so it lands in the cache — never a store. The one new thing realtime adds is the connection status, which is UI state.
Scope: server→client push (live lists, dashboards, notification badges). Collaborative editing / presence / CRDT is out of scope — see ./realtime-patterns.md.
1. Audit current state
grep -rnE "new WebSocket|EventSource|socket\.io|pusher|ably|@supabase.*realtime" src/ 2>/dev/null # existing realtime to wrap
ls src/libs/realtime.ts src/stores/useRealtimeStatusStore.ts .claude/stack-profile.md 2>/dev/null
grep -n "lists:" src/libs/queryKeys.ts 2>/dev/null # is the lists() factory accessor present?
grep -n "VITE_REALTIME_URL" src/libs/env.ts 2>/dev/null
Read .claude/stack-profile.md if present: frontend.framework decides the branch in step 3; backend.track of supabase means use Supabase Realtime behind the same seam (realtime-patterns.md, "When to deviate"); package_manager replaces pnpm.
Prerequisites: set-up-state-management (the queryClient, the queryKeys factory, the cache-as-truth boundary). Recommended: set-up-auth (the connection authenticates the same way the fetcher does) and validate-env (owns the VITE_REALTIME_URL read). Scattered new WebSocket / vendor calls in components are an audit finding — wrap them behind the seam (step 5).
2. Decide what to do
- No realtime → full setup (steps 4–7).
- Vendor SDK or raw
WebSocketcalled from components → introduce the seam and route calls through it. - Seam present but no reconnect/status handling → add the missing resilience (step 5).
- Everything present → confirm live data writes to the cache (not a store) and exit "Realtime already in place."
3. Detect framework
React → hook (src/hooks/) + Zustand status store. Vue → composable (src/composables/) + Pinia status store. The seam (src/libs/realtime.ts) is plain TS, identical for both.
4. Extend the env schema and the cache types
Env (validate-env in place — preferred). Add VITE_REALTIME_URL as optional (a ws:/wss: URL), so its absence disables realtime instead of failing the boot check:
// src/libs/env.ts — add to the schema object (blankToUndefined is defined at the top of that file)
VITE_REALTIME_URL: z.preprocess(blankToUndefined, z.url({ protocol: /^wss?$/ }).optional()),
Without validate-env, the seam reads import.meta.env.VITE_REALTIME_URL directly; note the deviation in the README and consider running validate-env.
Cache types. set-up-state-management already built the lists() / details() key factory the bridge needs (grep above). Replace the hand-written Todo type with a Zod schema, so pushed payloads can be validated by the same type the cache holds (in a feature layout the schema lives in features/todos/schemas/):
// src/libs/queryKeys.ts — replaces `export type Todo = …`; keep the rest of the file
import { z } from 'zod';
export const TodoSchema = z.object({ id: z.string(), text: z.string(), done: z.boolean() });
export type Todo = z.infer<typeof TodoSchema>;
5. Write the seam, the status store and the bridge
Copy these from ./realtime-code.md, framework branch as detected:
src/libs/realtime.ts— the seam: one shared WebSocket, topic routing, backoff reconnect with jitter, waits while offline, closes when the last listener leaves, no-op without a URL, validates the envelope of every frame.src/stores/useRealtimeStatusStore.ts— connection status (idle | connecting | open | reconnecting | offline), the only state realtime puts in a store.src/hooks/useRealtimeSync.ts(React) orsrc/composables/useRealtimeSync.ts(Vue) — the cache bridge: validates each pushed payload withTodoSchema, patches the entity withsetQueryData, invalidateslists(), mirrors status into the store, and refetches after every re-open that follows a gap.src/components/atoms/ConnectionStatus/— an announcedrole="status"badge forconnecting,reconnecting,offline.
The { topic, data } envelope and the subscribe / unsubscribe frames are an assumed protocol: change them in realtime.ts only, to match your backend or a vendor SDK (./realtime-patterns.md). The server must check the Origin header on the handshake: a browser sends same-site cookies on a WebSocket upgrade, so an unchecked endpoint is open to cross-site hijacking (../../core/_shared/security-baseline.md).
captureError is the seam from set-up-error-boundaries. If that skill has not run, run it first, or use console.error in the bridge.
6. Wire it at the app root
The bridge runs inside the Query provider. Mount it once.
React — in src/App.tsx
import { useRealtimeSync } from '@/hooks/useRealtimeSync';
export default function App() {
useRealtimeSync(); // once, under <QueryClientProvider> (set-up-state-management)
// …rest of the app
}
Vue — in src/App.vue
<script setup lang="ts">
import { useRealtimeSync } from '@/composables/useRealtimeSync';
useRealtimeSync(); // once; VueQueryPlugin + Pinia are installed in main.ts
</script>
7. Verify
pnpm typecheck # seam, bridge, store and cache types compile
Add the browser test from ./realtime-code.md ("Test") once configure-test-stack has run: it checks that valid frames reach the handler, malformed frames (null, non-JSON) are ignored, and a dropped socket reconnects and re-subscribes.
Manual: with VITE_REALTIME_URL set, run pnpm dev, push a todos event from the server, and confirm the list updates without a refresh. Kill the connection: the badge shows "Reconnecting…"; restore it: data refetches. Toggle the browser offline, then online: the same refetch happens. Unset VITE_REALTIME_URL: no socket opens and the app runs normally.
References
- ./realtime-code.md — the seam, store, bridge, badge and test, framework by framework.
- ./realtime-patterns.md — the cache-not-store rule, hybrid write, reconnect recovery, payload validation, and the SSE / vendor / high-volume / collaborative deviations.
- ../set-up-state-management/SKILL.md — the
queryClient+queryKeysfactory this writes through; the server/UI boundary. - ../set-up-auth/SKILL.md — how the connection authenticates (cookie on the handshake; token-as-first-frame variant).
- ../validate-env/SKILL.md — the
envseam that ownsVITE_REALTIME_URL. - ../set-up-error-boundaries/SKILL.md — the
captureErrorseam the bridge reports malformed payloads to. - ../_shared/conventions.md —
libs/seam location, thestores/rule, hooks vs composables.