Realtime is a synchronization mechanism, not the authoritative database.
Course navigationPhase 26⌄
Realtime
Add realtime synchronization without creating a second source of truth or leaking unauthorized events.
Phase outcome and project increment
Learning outcome
Add realtime synchronization without creating a second source of truth or leaking unauthorized events.
Concrete deliverable
Realtime task updates that merge into TanStack Query state with scoped subscriptions and reconnect behavior.
Principles to keep while coding
Subscribe only to events relevant to the current user and resource scope.
Design reconnect and duplicate-event behavior so clients can recover after temporary disconnection.
Reconcile realtime events with cached server state rather than maintaining an unrelated second data model.
Guided phase lab
Do these in order. Each step should end with evidence before the next one starts.
- 01
Choose the exact collaborative event that benefits from realtime rather than subscribing to everything.
- 02
Create a channel scoped to workspace/board context and define subscription lifecycle.
- 03
Feed events into query cache updates or invalidation instead of a parallel global store.
- 04
Deduplicate events that echo the current user's own mutation.
- 05
Test disconnect/reconnect and ensure a fresh server query repairs any missed events.
- 06
Verify authorization/data exposure boundaries for every subscription filter/channel.
Failures you should know how to diagnose
Realtime treated as database authority
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Subscriptions never cleaned up
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Events for other tenants visible to a client
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Duplicate optimistic + realtime updates
Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.
Lessons
The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.
Realtime Mental Model and When It Earns Its Place
Realtime Mental Model and When It Earns Its Place Volume 07 - Advanced Application Architecture The problem Realtime solves • User A changes a Task while User B is looking at the same Worksp
Broadcast Presence and Postgres Changes
Broadcast, Presence and Postgres Changes Volume 07 - Advanced Application Architecture Three capabilities Three Realtime capabilities - three different jobs Broadcast Postgres Changes Presen
TaskFlow Broadcast Architecture
TaskFlow Broadcast Architecture Volume 07 - Advanced Application Architecture End-to-end path TaskFlow Realtime data path User A NestJS Postgres DB trigger Realtime mutation rules + auth com
Database Trigger and Workspace Topic Design
Database Trigger and Workspace Topic Design Volume 07 - Advanced Application Architecture Trigger function CREATE OR REPLACE FUNCTION public.broadcast_task_changes() RETURNS trigger SECURITY
Private Channels and Realtime Authorization
Private Channels and Realtime Authorization Volume 07 - Advanced Application Architecture Authorization path Private workspace channel authorization Supabase JWT Realtime join RLS policy Mem
Next.js Subscription Lifecycle
Next.js Subscription Lifecycle Volume 07 - Advanced Application Architecture Client hook export function useWorkspaceTaskRealtime( workspaceId: string, ) { const queryClient = useQueryClient
TanStack Query Integration
TanStack Query Integration: Invalidate First, Patch Selectively Volume 07 - Advanced Application Architecture Default strategy Realtime event + TanStack Query Broadcast event Handler invalid
Realtime with Optimistic Updates
Realtime with Optimistic Updates Volume 07 - Advanced Application Architecture The same mutation can generate two signals User A clicks DONE -> optimistic UI predicts DONE -> HTTP PATCH /tas
Multi Tenant Channel Strategy
Multi-Tenant Workspace Channel Strategy Volume 07 - Advanced Application Architecture Topic design workspace:<workspaceId>:tasks Examples: workspace:11111111-...:tasks workspace:22222222-...
Presence and Ephemeral Collaboration
Presence and Ephemeral Collaboration Volume 07 - Advanced Application Architecture Presence is not Task persistence Presence answers questions like “Who is viewing this board?” or “Who is on
Reliability Scaling and Troubleshooting
Reliability, Scaling and Troubleshooting Volume 07 - Advanced Application Architecture Realtime is lossy from the app perspective Clients can sleep, lose Wi-Fi, change networks, suspend back
Hands On Lab and Phase Exit
Hands-On Lab and Phase Exit Volume 07 - Advanced Application Architecture Lab A - Enable database-triggered Broadcast • Apply the Task trigger migration. • Create the Realtime authorization
Guided Build Lab
Purpose Add realtime synchronization without creating a second source of truth or leaking unauthorized events.
Debugging Workshop
Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.
Phase Synthesis & Independent Challenge
Mission Rebuild the capability of Phase 26 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?
Mastery Capstone Lab
Integrate Phase 26 without following a transcript.
Exercises that increase independence
Choose the exact collaborative event that benefits from realtime rather than subscribing to everything.
Feed events into query cache updates or invalidation instead of a parallel global store.
reproduce and diagnose — Realtime treated as database authority.
deliver Realtime task updates that merge into TanStack Query state with scoped subscriptions and reconnect behavior.
Can scope subscriptions and Can reconcile realtime with server cache.
Do not leave the phase only because you reached the bottom
Phase exit criteria
Scenario and concept quiz
These questions test engineering choices rather than whether you remember a lesson title.
Reference files
Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.
reference/TaskFlow_Realtime_Snapshot/POSTGRES_CHANGES_ALTERNATIVE.md
# Postgres Changes alternative
For a small prototype you can subscribe directly:
```ts
const channel = supabase
.channel("task-postgres-changes")
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "Task",
},
() => queryClient.invalidateQueries({ queryKey: taskKeys.all }),
)
.subscribe();
```
TaskFlow Phase 26 prefers Broadcast for database-change delivery because Supabase currently recommends Broadcast for scalability/security. Keep this alternative for comparison and troubleshooting, not as the production default.reference/TaskFlow_Realtime_Snapshot/README.md
# TaskFlow Phase 26 Realtime Snapshot This reference implements Realtime as a freshness layer, not as a new backend. - Writes still go Browser -> NestJS -> business rules -> repository -> PostgreSQL. - PostgreSQL trigger emits private Supabase Broadcast events after Task changes. - Browser subscribes to one private Workspace Task channel. - Realtime Authorization checks existing TaskFlow Membership. - Realtime events invalidate TanStack Query Task keys. - Reconnect/subscription success triggers an authoritative refetch. - No direct browser Task writes are introduced.
reference/TaskFlow_Realtime_Snapshot/REALTIME_DECISION_TABLE.md
# Realtime Decision Table | Need | Default | |---|---| | Coworker Task changes should appear quickly | Private Broadcast + query invalidation | | Tiny prototype / simplest DB CDC | Postgres Changes | | Who is online / viewing board | Presence | | Rare settings changes | Normal query refetch only | | Payroll finalization | HTTP mutation + explicit confirmation; Realtime only as notification | | High-frequency cursor movement | Broadcast, not database persistence | Realtime is optional. HTTP correctness must remain intact without it.
reference/TaskFlow_Realtime_Snapshot/SOURCES.md
# Sources - [Supabase - Subscribing to Database Changes](https://supabase.com/docs/guides/realtime/subscribing-to-database-changes) - Broadcast is the recommended database-change method for scalability and security; Postgres Changes is simpler but scales less well. Includes realtime.broadcast_changes trigger pattern. - [Supabase - Realtime Authorization](https://supabase.com/docs/guides/realtime/authorization) - Private channels use RLS policies on realtime.messages; permissions are evaluated when the client joins a topic. - [Supabase - Realtime Concepts](https://supabase.com/docs/guides/realtime/concepts) - Defines Channels, topics, Broadcast, Presence and Postgres Changes. - [Supabase - Realtime Architecture](https://supabase.com/docs/guides/realtime/architecture) - Describes Broadcast from Postgres, WAL/replication behavior and Realtime internals. - [Supabase - Broadcast](https://supabase.com/docs/guides/realtime/broadcast) - Documents database-triggered Broadcast, private channels, replay and broadcast payloads. - [Supabase - Realtime Protocol](https://supabase.com/docs/guides/realtime/protocol) - Documents access-token refresh, server events, reconnection behavior and broadcast protocol. - [Supabase JS - removeChannel](https://supabase.com/docs/reference/javascript/removechannel) - Documents explicit channel teardown and removal. - [Supabase JS - setAuth](https://supabase.com/docs/reference/javascript/setauth) - Documents the JWT used for Realtime authorization and RLS. - [Supabase - Realtime Limits](https://supabase.com/docs/guides/realtime/limits) - Documents connection/message limits and Postgres Changes scaling considerations. - [TanStack Query - QueryClient](https://tanstack.com/query/latest/docs/reference/QueryClient) - Provides invalidateQueries and setQueryData used to synchronize Realtime events with query cache.
reference/TaskFlow_Realtime_Snapshot/apps/web/src/app/(app)/w/[workspaceSlug]/layout.phase26.tsx
import type { ReactNode } from "react";
import { WorkspaceTaskRealtimeProvider } from "@/components/realtime/workspace-task-realtime-provider";
import { getAuthorizedWorkspace } from "@/lib/workspaces/get-authorized-workspace";
export default async function WorkspaceLayout({
children,
params,
}: {
children: ReactNode;
params: Promise<{ workspaceSlug: string }>;
}) {
const { workspaceSlug } = await params;
const workspace = await getAuthorizedWorkspace(workspaceSlug);
return (
<WorkspaceTaskRealtimeProvider workspaceId={workspace.id}>
{children}
</WorkspaceTaskRealtimeProvider>
);
}reference/TaskFlow_Realtime_Snapshot/apps/web/src/components/realtime/workspace-task-realtime-provider.tsx
"use client";
import type { ReactNode } from "react";
import { useWorkspaceTaskRealtime } from "@/hooks/use-workspace-task-realtime";
export function WorkspaceTaskRealtimeProvider({
workspaceId,
children,
}: {
workspaceId: string;
children: ReactNode;
}) {
useWorkspaceTaskRealtime(workspaceId);
return children;
}reference/TaskFlow_Realtime_Snapshot/apps/web/src/hooks/use-workspace-task-realtime.ts
"use client";
import { useEffect, useMemo } from "react";
import type { RealtimeChannel } from "@supabase/supabase-js";
import { useQueryClient } from "@tanstack/react-query";
import { createBrowserSupabaseClient } from "@/lib/supabase/client";
import { taskKeys } from "@/lib/query/task-keys";
export function useWorkspaceTaskRealtime(workspaceId: string) {
const queryClient = useQueryClient();
const supabase = useMemo(() => createBrowserSupabaseClient(), []);
useEffect(() => {
let channel: RealtimeChannel | undefined;
let disposed = false;
const refresh = () => {
void queryClient.invalidateQueries({
queryKey: taskKeys.all,
});reference/TaskFlow_Realtime_Snapshot/database/realtime/01_task_broadcast.sql
-- Phase 26: broadcast committed Task changes to Workspace-scoped private topics.
CREATE OR REPLACE FUNCTION public.broadcast_task_changes()
RETURNS trigger
SECURITY DEFINER
SET search_path = ''
LANGUAGE plpgsql
AS $$
DECLARE
project_id uuid;
workspace_id uuid;
BEGIN
project_id := CASE
WHEN TG_OP = 'DELETE' THEN OLD."projectId"
ELSE NEW."projectId"
END;
SELECT p."workspaceId"
INTO workspace_id
FROM public."Project" preference/TaskFlow_Realtime_Snapshot/database/realtime/02_realtime_authorization.sql
-- Phase 26: authorize private Workspace Task topics from TaskFlow membership.
-- This policy protects Realtime channel joins/reads. NestJS still authorizes API writes.
CREATE OR REPLACE FUNCTION public.can_receive_task_topic(
requested_topic text
)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = ''
AS $$
SELECT EXISTS (
SELECT 1
FROM public."Membership" m
JOIN public."User" u
ON u.id = m."userId"
WHERE u."authSubject" = (SELECT auth.uid())::text
AND requested_topic =
'workspace:' || m."workspaceId"::text || ':tasks'reference/TaskFlow_Realtime_Snapshot/database/realtime/03_postgres_changes_learning_alternative.sql
-- Learning-only alternative: Postgres Changes requires the table in the publication. -- TaskFlow production direction in Phase 26 prefers Broadcast for database changes. ALTER PUBLICATION supabase_realtime ADD TABLE public."Task"; -- If you require old row values for UPDATE/DELETE payloads: -- ALTER TABLE public."Task" REPLICA IDENTITY FULL;
reference/TaskFlow_Realtime_Snapshot/tests/realtime-test-matrix.md
# Phase 26 Realtime Test Matrix | Scenario | Expected | |---|---| | User A and B belong to same Workspace | Both can subscribe to private Task topic | | User B does not belong to Workspace | User B cannot receive Workspace Task broadcasts | | User A updates Task through NestJS | DB trigger emits UPDATE after commit | | User B is online | Task query invalidates/refetches automatically | | User B offline during several changes | Reconnect/SUBSCRIBED refetch catches current state | | Local optimistic update succeeds | Realtime + mutation invalidations still converge to server truth | | Local optimistic update gets 409 | Local rollback works; Realtime does not override business error semantics | | Workspace switch | Old channel removed; new Workspace channel joined | | Component unmount | removeChannel called | | Bad topic / no membership | subscription denied / no data delivered | | CHANNEL_ERROR or TIMED_OUT | full error logged without token | | Realtime disabled | normal HTTP query/mutation flow still works |