Realtime Architecture
Decision Framework: Realtime vs Polling vs SSE
Choose the right transport for each data type:
| Data Type | Transport | Why |
|---|---|---|
| Spectator events (live feed) | Supabase Broadcast | Ephemeral, high-frequency, many consumers |
| Leaderboard rank changes | Postgres Changes | Persisted, low-frequency, needs RLS |
| Challenge status transitions | Postgres Changes | Persisted, low-frequency, triggers UI state |
| Notification count (nav bell) | Postgres Changes | Persisted, per-user, needs RLS |
| Live spectator count | Presence | Ephemeral, aggregate count, auto-cleanup |
| Challenge timer sync | Broadcast | Ephemeral, server-authoritative, low-frequency |
| Agent online/offline status | Polling (30s) | Derived from heartbeat, not worth a channel |
| ELO history chart data | HTTP fetch | Static after calculation, no live updates |
| Dashboard stats | HTTP fetch + 30s poll | Aggregated, too expensive for realtime |
| Admin job queue | Polling (5s) | Admin-only, low user count, simple |
When NOT to Use Realtime
- Data that changes less than once per minute → poll
- Data visible to only 1 user with no urgency → poll
- Large payloads (> 100KB) → HTTP fetch
- Data requiring complex JOINs → HTTP fetch (Postgres Changes only sends the changed row)
For detailed patterns on each transport, see:
- Channel design & broadcast → references/channels.md
- Security & anti-spoofing → references/security.md
- Performance & scaling → references/performance.md
- Client patterns (React hooks) → references/client-patterns.md
Quick Reference — Code Review Checklist
- Every Broadcast channel uses
config: { private: true }— never public - Every
useEffectwith.subscribe()returns cleanup with.unsubscribe() - Realtime event handlers use functional state updates (
setData(prev => ...)) - Spectator delay is 30s server-side, not client
setTimeout - Max 5 concurrent channels per user enforced in RealtimeProvider
- Reconnection uses exponential backoff with jitter
- High-frequency events (spectator feed) are batched to ≤ 2 broadcasts/second
- Postgres Changes filters use column filters to reduce payload
- Presence tracks only aggregate counts, never individual user details
- All channels unsubscribe on route change / component unmount