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:
- Subscribe to live changes on the
taskstable for the signed-in user - Update the task list instantly without any page refresh
- Handle task creation, updates, and deletions from the live stream
- Clean up the subscription correctly when the component unmounts
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:
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:
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.
BlocWeave