specs/pf/specs/PF-66-platform-realtime-layer.md
Plan: specs/pf/plans/PF-66-platform-realtime-layer-PLAN.md
Purpose: Step-by-step guide for migrating existing ad-hoc Supabase Realtime implementations to the shared @/platform/realtime module. Each section shows the exact before/after code for one implementation, the verification steps, and the expected outcome.
Table of Contents
- Overview
- Migration Principles
- Migration 1: Notification Hooks (PF)
- Migration 2: Dashboard Widget Hook (PF)
- Migration 3: PendingActionsWidget Inline Subscription (PF)
- Migration 4: Workflow Execution Hooks (FW)
- Migration 5: SMS Messages Hook (CE)
- Migration 6: Financial Close Broadcast (FA)
- Adding New Real-Time Features
- Deprecation Timeline
- Troubleshooting
Overview
The@/platform/realtime module provides three shared hooks that replace all direct supabase.channel() usage:
Import:
Migration Principles
- Internal refactors only — External hook APIs (return types, option types) MUST NOT change
- Zero regressions — Run existing tests before and after each migration
- One hook at a time — Migrate and verify each hook separately, commit after each
- Core ownership — PF team migrates PF hooks; FW/CE teams migrate their own hooks
- Organization filter — Always include
organization_idin the subscription filter (defense-in-depth)
Migration 1: Notification Hooks (PF)
Owner: PF team Files:src/platform/notifications/useNotifications.ts
Impact: Reduces 3 channels to 1 for pf_notifications
1a: useNotifications (list query + subscription)
BEFORE:
1b: useUnreadCount (count query + subscription)
BEFORE: Same inline useEffect pattern with a separate channel 'notifications-unread-changes'.
AFTER:
Key insight: Both hooks subscribe to the same table (pf_notifications) with no filter. TheChannelManagerautomatically multiplexes them onto a single Supabase channel.
1c: useNotificationToasts (INSERT-only toast)
BEFORE:
Verification
channel=eq.in_app filter creates a distinct channel name).
Migration 2: Dashboard Widget Hook (PF)
Owner: PF team Files:src/platform/dashboard/hooks/useRealtimeWidget.ts
BEFORE:
AFTER:
BedCensusWidget, EmployeeCountWidget, and all other consumers work unchanged.
Verification
Migration 3: PendingActionsWidget Inline Subscription (PF)
Owner: PF team Files:src/platform/dashboard/widgets/PendingActionsWidget.tsx
BEFORE:
AFTER:
useEffect block (lines 43-66).
Verification
Migration 4: Workflow Execution Hooks (FW)
Owner: FW team (or PF with migration guide) Files:src/cores/fw/hooks/useRealtimeExecutions.ts
4a: useRealtimeExecutions (list with optimistic merge)
BEFORE: ~90 lines of inline channel setup with INSERT/UPDATE/DELETE switch statement.
AFTER:
4b: useRealtimeExecution (single record)
AFTER:
Verification
Migration 5: SMS Messages Hook (CE)
Owner: CE team (or PF with migration guide) Files:src/cores/ce/hooks/useRealtimeSmsMessages.ts
BEFORE: ~100 lines of inline channel setup with local state merge and matchesFilters logic.
AFTER:
Verification
Migration 6: Financial Close Broadcast (FA)
Owner: PF team (or FA team) Files:src/cores/fa/wizards/financial-close-setup/FinancialCloseSetupWizardPage.tsx
BEFORE:
AFTER:
Verification
Adding New Real-Time Features
After migration, adding real-time to any page takes 3-5 lines:Pattern A: Query Invalidation (most common)
Pattern B: Optimistic Local Merge (chat / messaging)
Pattern C: Ephemeral Broadcast (typing indicators, activity pulses)
Pattern D: Presence (who’s viewing this record)
Deprecation Timeline
What Gets Deprecated
What Stays
Troubleshooting
”Channel limit exceeded” warning
TheRealtimeProvider logs a warning when channel count exceeds maxChannels (default: 20). This is advisory, not a hard block.
Fix: Check for components that mount/unmount rapidly, causing orphaned subscriptions. Verify useRealtimeSubscription cleanup runs on unmount.
Notification toasts stopped appearing
Check: Verify thefilter: { channel: 'in_app' } is correctly passed. The shared hook may create a different channel name than the old inline code if the filter format changed.
”Not connected” badge when data is actually fresh
Check: TheisConnected state may take a moment to update after subscription. Verify the RealtimeProvider is mounted in the component tree above the component using the hook.
Multiple channels for the same table
Check: Verify channel names match. Two subscriptions to the same table but with different filters will create separate channels (by design). UseuseConnectionStatus() to inspect total channel count.
References
- Spec:
specs/pf/specs/PF-66-platform-realtime-layer.md - Plan:
specs/pf/plans/PF-66-platform-realtime-layer-PLAN.md - Tasks:
specs/pf/tasks/PF-66-platform-realtime-layer-TASKS.md - Strategy: docs/architecture/REAL_TIME_ARCHITECTURE.md
- Supabase Realtime Docs: https://supabase.com/docs/guides/realtime
Pattern 2: Broadcast (Fire-and-Forget Events)
Before
After
Pattern 3: Query Invalidation on Change
Before
After
Migration Checklist
- Replace
supabase.channel()with appropriate hook (useRealtimeSubscription,useRealtimeBroadcast, oruseRealtimePresence) - Remove
useEffectcleanup (supabase.removeChannel()) — hooks handle this - Remove
import { supabase }if no longer needed - Remove
useQueryClientif only used for invalidation (usequery.refetch()instead) - Verify
autoInjectOrgIdbehavior (defaulttrue— setfalsefor user-scoped subscriptions) - Add
RealtimeConnectionBadgeto page headers where appropriate - Run
npm run lintto verify nosupabase.channel()violations
Troubleshooting
”Channel limit exceeded” errors
Symptom: Console warning about exceeding max channels. Diagnose: CheckuseConnectionStatus().activeChannels — if it’s near or above realtime_max_channels (default 20), you have too many concurrent subscriptions.
Fix:
- Increase
realtime_max_channelsinpf_module_settings(max 100) - Review components for redundant subscriptions (e.g., multiple hooks on the same table)
- Ensure hooks are cleaned up on unmount (they should be if using
useRealtimeSubscription)
Connection debugging with useConnectionStatus
<RealtimeConnectionBadge isConnected={health.activeChannels > 0} /> to visually monitor.
Missing RealtimeProvider
Symptom: Hooks silently fail or channelManager is undefined.
Fix: Ensure <RealtimeProvider> is mounted inside <OrganizationProvider> in App.tsx. The provider needs organization context for tenant-scoped settings.
Fallback when realtime is unavailable
If the WebSocket connection drops, hooks stop receiving events. Add a polling fallback:Migration error guidance
See
src/platform/realtime/README.md for complete API docs.
API Reference
Seesrc/platform/realtime/README.md for complete API documentation.