What you'll build

Right now, the only thing stopping User A from reading User B's tasks is a userId filter in your server actions. If someone calls your Supabase client directly with the anon key and skips that filter, they can read everything.

In this part you'll add Row Level Security (RLS) policies that enforce data isolation at the database level — so no query, from any client, can return rows that don't belong to the requesting user. You'll also add role-based access so team members can be granted access to a project's tasks.

By the end of this part:

How RLS + Firebase UID works

Supabase RLS policies run inside Postgres using the auth.uid() function — which returns the user ID from the JWT the client sends. By default this is a Supabase-issued JWT.

We're using Firebase Auth, which issues its own JWTs. To make Supabase trust Firebase tokens, we set up a custom JWT verifier. Supabase lets you configure a third-party auth provider (or use a custom JWT secret) to do exactly this.

In the Supabase dashboard, go to Authentication → Sign In with Firebase and follow the wizard. Supabase will verify incoming Firebase ID tokens and expose auth.uid() as the Firebase UID.

Option B — Pass UID via a custom claim

If Option A isn't available on your plan, pass the Firebase UID as an app-metadata claim when creating the Supabase session. Your server actions already have the UID — you can inject it as a custom request.jwt.claims header.

Either approach makes auth.uid() in Postgres equal to the Firebase UID stored in tasks.user_id.

Add the RLS policies

Replace the permissive policy from Part 4 with proper per-user policies:

-- Drop the permissive policy from Part 4
drop policy if exists "Users can read own tasks" on tasks;

-- Users can only see their own tasks
create policy "select own tasks"
  on tasks for select
  using (auth.uid()::text = user_id);

-- Users can only insert tasks for themselves
create policy "insert own tasks"
  on tasks for insert
  with check (auth.uid()::text = user_id);

-- Users can only update their own tasks
create policy "update own tasks"
  on tasks for update
  using (auth.uid()::text = user_id);

-- Users can only delete their own tasks
create policy "delete own tasks"
  on tasks for delete
  using (auth.uid()::text = user_id);

Run this in the Supabase SQL Editor.

Add a projects table with team membership

A task manager is more useful when tasks belong to projects that teams can share:

create table projects (
  id         uuid primary key default gen_random_uuid(),
  name       text not null,
  owner_id   text not null,
  created_at timestamptz not null default now()
);

create table project_members (
  project_id uuid references projects(id) on delete cascade,
  user_id    text not null,
  role       text not null default 'member'
                check (role in ('admin', 'member')),
  joined_at  timestamptz not null default now(),
  primary key (project_id, user_id)
);

-- Add project_id to tasks (nullable — tasks can be personal or in a project)
alter table tasks add column project_id uuid references projects(id) on delete set null;

Add team-aware RLS policies

BlocWeave prompt

Add Row Level Security policies to the projects and project_members tables in Supabase. For projects: - Owners can do everything (select, insert, update, delete) - Members (in project_members) can select For tasks with a project_id set: - Project admins can select, insert, update, delete - Project members can select and update, but not delete For tasks without a project_id (personal tasks): - Only the owner (user_id) can do anything Write the SQL policies. Use auth.uid()::text for comparisons against text user_id columns. Use EXISTS subqueries to check project_members for membership and role.

Check the generated SQL carefully before running it — RLS policies are security-critical. The EXISTS subquery pattern for checking membership looks like this:

exists (
  select 1 from project_members
  where project_members.project_id = tasks.project_id
    and project_members.user_id = auth.uid()::text
    and project_members.role = 'admin'
)

Update server actions to use the anon key

Now that RLS is enforcing access control, server actions can switch from the service role key to the anon key — the database will reject any query that violates a policy, regardless of which client is used:

BlocWeave prompt

Update src/lib/supabase.ts. The createServerClient function should now use NEXT_PUBLIC_SUPABASE_ANON_KEY instead of SUPABASE_SERVICE_ROLE_KEY. The server actions in src/lib/task-actions.ts need to pass the user's Firebase ID token as a Bearer token in the Authorization header when creating the Supabase client, so that auth.uid() in RLS policies resolves to the correct Firebase UID.

Verify isolation

Test that RLS is working by bypassing your app and calling Supabase directly. In your browser console on the dashboard page:

const { createClient } = await import("@supabase/supabase-js");
const sb = createClient(
  "https://xxxxxxxxxxxx.supabase.co",
  "your_anon_key"
);
const { data } = await sb.from("tasks").select("*");
console.log(data);

Without a valid auth token, this should return an empty array or a permission error — not another user's tasks. If it returns all tasks, check that RLS is enabled on the table and your policies are correct.

Project structure after Part 5

src/
  lib/
    supabase.ts       ← createServerClient now uses anon key + auth header
    task-actions.ts   ← passes Firebase ID token to Supabase client
-- Tables
tasks            ← per-user and per-project RLS policies
projects         ← owner-based RLS
project_members  ← membership table, role column

What's next

In Part 6 — the final part — you'll deploy the finished app to Cloudflare Pages. BlocWeave will handle the build configuration, environment variable setup, and the Cloudflare Pages config file.