When to use it
The product needs its own data owned by a user, for example notes, projects or saved items.
Rules to keep
- Migrations are append-only: create a new file in
supabase/migrations/, never edit one that has already been applied. - RLS is the authorization boundary. Every new table in
publicenables row level security in the same migration that creates it. - The owner comes from the session (
auth.uid()), never from the request body. - Server code uses the per-request client in
src/lib/supabase/server.ts, which runs as the signed-in user.SUPABASE_SERVICE_ROLE_KEYbypasses RLS: do not use it for user data and never import it in client code.
Steps
Create
supabase/migrations/<timestamp>_<name>.sqlwith a timestamp later than the latest migration. For anotestable:create table public.notes ( id uuid primary key default gen_random_uuid(), owner_id uuid not null default auth.uid() references auth.users(id) on delete cascade, body text not null check (char_length(body) between 1 and 2000), created_at timestamptz not null default now() ); create index notes_owner_id_idx on public.notes (owner_id); alter table public.notes enable row level security; create policy "notes_select_own" on public.notes for select to authenticated using ((select auth.uid()) = owner_id); create policy "notes_insert_own" on public.notes for insert to authenticated with check ((select auth.uid()) = owner_id); create policy "notes_update_own" on public.notes for update to authenticated using ((select auth.uid()) = owner_id) with check ((select auth.uid()) = owner_id); create policy "notes_delete_own" on public.notes for delete to authenticated using ((select auth.uid()) = owner_id); revoke all on public.notes from anon;Write only the policies the product needs: a table that users read but never change has only a
selectpolicy. Write(select auth.uid())as inprofiles, so Postgres evaluates it once per query instead of once per row.If users must not change some columns, follow
profiles:revoke updateon the table, thengrant update (<columns>)only on the editable ones.Read and write from a Server Component or a Route Handler with
createClient()fromsrc/lib/supabase/server.ts. As insrc/app/api/me/route.ts, callauth.getUser()first and answer 401 without a user. Do not filter on anowner_idtaken from the request: RLS already limits the rows.Apply the migration (
supabase db push, or the SQL editor of the project) and check isolation with two accounts: user A creates a row, user B must not see, update or delete it, and an anonymous request gets no rows.Put validation of the input in a pure module under
src/lib/and test it intest/with Vitest; the tests do not use the network.Describe the table and its rules in
DATABASE.md.
Done when
- the verification commands pass;
- the new table has RLS enabled and one policy per allowed operation;
- the two-account check in step 5 passed;
DATABASE.mddescribes the table.
