Skip to main content
Version: 1.0.0 Last Updated: 2026-02-09 Spec: 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

The @/platform/realtime module provides three shared hooks that replace all direct supabase.channel() usage: Import:

Migration Principles

  1. Internal refactors only — External hook APIs (return types, option types) MUST NOT change
  2. Zero regressions — Run existing tests before and after each migration
  3. One hook at a time — Migrate and verify each hook separately, commit after each
  4. Core ownership — PF team migrates PF hooks; FW/CE teams migrate their own hooks
  5. Organization filter — Always include organization_id in 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:
AFTER:

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. The ChannelManager automatically multiplexes them onto a single Supabase channel.

1c: useNotificationToasts (INSERT-only toast)

BEFORE:
AFTER:

Verification

Expected outcome: 3 Supabase channels reduced to 1 (or 2 if the 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:

External API is identicalBedCensusWidget, 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:

Delete the entire 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

The RealtimeProvider 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 the filter: { 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: The isConnected 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). Use useConnectionStatus() to inspect total channel count.

References


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, or useRealtimePresence)
  • Remove useEffect cleanup (supabase.removeChannel()) — hooks handle this
  • Remove import { supabase } if no longer needed
  • Remove useQueryClient if only used for invalidation (use query.refetch() instead)
  • Verify autoInjectOrgId behavior (default true — set false for user-scoped subscriptions)
  • Add RealtimeConnectionBadge to page headers where appropriate
  • Run npm run lint to verify no supabase.channel() violations

Troubleshooting

”Channel limit exceeded” errors

Symptom: Console warning about exceeding max channels. Diagnose: Check useConnectionStatus().activeChannels — if it’s near or above realtime_max_channels (default 20), you have too many concurrent subscriptions. Fix:
  • Increase realtime_max_channels in pf_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

Use <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

See src/platform/realtime/README.md for complete API documentation.