What you'll build

In this part you'll replace the static server-rendered task list with a real-time version that updates the moment a task is created, updated, or deleted — whether the change came from the current browser tab or another device entirely.

By the end of this part your dashboard will:

How Supabase Realtime works

Supabase Realtime listens to Postgres's Write-Ahead Log (WAL) and pushes row-level change events to connected clients over a WebSocket. You subscribe to a channel, filter by table and conditions, and receive INSERT, UPDATE, and DELETE events as they happen.

The key requirement: Realtime requires Row Level Security to be enabled on the table you're subscribing to. Without RLS, Supabase won't stream changes to the browser. We'll enable a permissive RLS policy for now (anyone can read their own rows) and harden it properly in Part 5.

Enable RLS and Realtime on the tasks table

In the Supabase SQL Editor, run:

-- Enable RLS
alter table tasks enable row level security;

-- Allow users to select their own tasks
-- (uses the user_id column we filter by in server actions)
create policy "Users can read own tasks"
  on tasks for select
  using (true);

-- Enable realtime publication for the tasks table
alter publication supabase_realtime add table tasks;

The `using (true)` policy is intentionally permissive for now — it lets all authenticated database connections read any row. Part 5 replaces this with proper per-user isolation using a custom JWT claim.

Convert the dashboard to a client component

The real-time subscription uses a WebSocket, which only works in the browser. The task list needs to move from a server component to a client component:

BlocWeave prompt

Refactor src/app/dashboard/page.tsx for real-time updates. 1. Create src/app/dashboard/TaskList.tsx — a client component ("use client") that: - Accepts `initialTasks: Task[]` and `userId: string` as props - Stores tasks in useState, initialised from initialTasks - In a useEffect, subscribes to the Supabase Realtime channel "tasks:{userId}" filtering on table "tasks" and column user_id equal to userId - Handles INSERT events by prepending the new task to state - Handles UPDATE events by replacing the matching task in state - Handles DELETE events by removing the matching task from state - Cleans up the subscription in the useEffect return function - Renders the task list and the create form (move the UI from page.tsx) 2. Update src/app/dashboard/page.tsx — keep it as a server component; fetch initial tasks on the server using getTasks and pass them as props to TaskList

Review the subscription code

The generated useEffect should look roughly like this:

useEffect(() => {
  const channel = supabase
    .channel(`tasks:${userId}`)
    .on(
      "postgres_changes",
      {
        event: "*",
        schema: "public",
        table: "tasks",
        filter: `user_id=eq.${userId}`,
      },
      (payload) => {
        if (payload.eventType === "INSERT") {
          setTasks((prev) => [payload.new as Task, ...prev]);
        } else if (payload.eventType === "UPDATE") {
          setTasks((prev) =>
            prev.map((t) => (t.id === payload.new.id ? (payload.new as Task) : t))
          );
        } else if (payload.eventType === "DELETE") {
          setTasks((prev) => prev.filter((t) => t.id !== payload.old.id));
        }
      }
    )
    .subscribe();

  return () => { supabase.removeChannel(channel); };
}, [userId]);

If BlocWeave generates something structurally different, compare against this pattern. The key things to check: the filter syntax (column=eq.value), the removeChannel cleanup, and that each event type correctly mutates the local state.

Test real-time updates

Open the dashboard in two browser tabs. In Tab 1, create a task. Without touching Tab 2, the task should appear there instantly.

Then open the Supabase dashboard → Table Editor → tasks. Edit a row directly from the dashboard. The change should appear in your app within a second.

**Supabase Realtime requires the anon key** in the browser client, not the service role key. If you use the service role key in `createBrowserClient()`, the WebSocket connection will be rejected. Check `src/lib/supabase.ts` and confirm `createBrowserClient()` uses `NEXT_PUBLIC_SUPABASE_ANON_KEY`.

Optimistic UI for faster feel

The real-time stream handles updates from other tabs, but actions the current user takes have a round-trip delay (server action → Supabase → Realtime event back to client). Add optimistic updates for a snappier feel:

BlocWeave prompt

In src/app/dashboard/TaskList.tsx, add optimistic updates for task creation and deletion using React's useOptimistic hook. When the user submits the create form, immediately add a temporary task to the list with a "saving" indicator. When the real INSERT event arrives from Supabase, replace the temporary entry. Do the same for deletions — remove the task from the list immediately on click, and restore it if the server action fails.

Project structure after Part 4

src/
  app/
    dashboard/
      page.tsx         ← server component: fetch initial tasks, pass to TaskList
      TaskList.tsx     ← client component: real-time subscription, task UI

What's next

In Part 5 we add Row Level Security properly — a Supabase RLS policy that uses the Firebase UID from the user's session to enforce per-user data isolation at the database level, so even a compromised anon key can't read another user's tasks.