Course navigationPhase 26
Volume 07 · Phase 26

Realtime

Add realtime synchronization without creating a second source of truth or leaking unauthorized events.

16lessons + labs
4klesson words
11source references
What you will ship

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.

Figure 26.1 — Where this phase lives in TaskFlow
database/event source
Supabase Realtime channel
authorized/scoped subscription
event handler
query cache reconcile
UI
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

Realtime is a synchronization mechanism, not the authoritative database.

02

Subscribe only to events relevant to the current user and resource scope.

03

Design reconnect and duplicate-event behavior so clients can recover after temporary disconnection.

04

Reconcile realtime events with cached server state rather than maintaining an unrelated second data model.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    Choose the exact collaborative event that benefits from realtime rather than subscribing to everything.

  2. 02

    Create a channel scoped to workspace/board context and define subscription lifecycle.

  3. 03

    Feed events into query cache updates or invalidation instead of a parallel global store.

  4. 04

    Deduplicate events that echo the current user's own mutation.

  5. 05

    Test disconnect/reconnect and ensure a fresh server query repairs any missed events.

  6. 06

    Verify authorization/data exposure boundaries for every subscription filter/channel.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

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.

Failure drill 2

Subscriptions never cleaned up

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 3

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.

Failure drill 4

Duplicate optimistic + realtime updates

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Watch-order style

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.

01

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

2 min
02

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

2 min
03

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

2 min
04

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

2 min
05

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

2 min
06

Next.js Subscription Lifecycle

Next.js Subscription Lifecycle Volume 07 - Advanced Application Architecture Client hook export function useWorkspaceTaskRealtime( workspaceId: string, ) { const queryClient = useQueryClient

2 min
07

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

2 min
08

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

2 min
09

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

2 min
10

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

2 min
11

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

2 min
12

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

2 min
LAB

Guided Build Lab

Purpose Add realtime synchronization without creating a second source of truth or leaking unauthorized events.

3 min
LAB

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.

2 min
LAB

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?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 26 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

Choose the exact collaborative event that benefits from realtime rather than subscribing to everything.

Practice

Feed events into query cache updates or invalidation instead of a parallel global store.

Debug

reproduce and diagnose — Realtime treated as database authority.

Independent

deliver Realtime task updates that merge into TanStack Query state with scoped subscriptions and reconnect behavior.

Explain-back

Can scope subscriptions and Can reconcile realtime with server cache.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 26's engineering goal?
02Which practice best matches Phase 26's engineering goal?
03Which practice best matches Phase 26's engineering goal?
04Which practice best matches Phase 26's engineering goal?
05You are working through the Phase 26 lab. Which next action is the most disciplined step?
06You are working through the Phase 26 lab. Which next action is the most disciplined step?
07You are working through the Phase 26 lab. Which next action is the most disciplined step?
08You are working through the Phase 26 lab. Which next action is the most disciplined step?
Source snapshot

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" p
reference/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 |