All posts
Internet Fundamentals

Row-Level Security: The Check Between Your Users' Data and Everyone Else

By

October 26, 2026

6 min read

Apps on Supabase, Firebase and similar platforms let the browser talk to the database directly. That is fine only if the database enforces who sees what, because anyone can call the same API without your UI. How row-level security works, the gaps that catch people (views, functions, storage), and a ten-minute test.

Internet Fundamentals
Row-Level Security
Supabase
PostgreSQL
Security

Your UI is not a security boundary. If the app queries the database from the browser with a publishable key, anyone can copy that key from the page and send their own queries. What comes back depends entirely on the database's rules.

How it works in Supabase and Postgres

  • Requests with the publishable key run as the anon Postgres role; signed-in users as authenticated. Policies decide what each role may do, row by row.

  • Supabase's documentation is blunt: a table in an exposed schema without RLS "is readable and writable by any role with a grant on it."

  • RLS enabled with no policies denies everything through the API. That is the safe failure.

  • Tables created in SQL need alter table ... enable row level security; do not assume the default.

Want a second opinion on this for your system?

Book a free 30-minute call. You'll leave knowing what we'd do first and roughly what it costs, whether or not you hire us.

A typical policy

For user-owned rows, a select policy using ((select auth.uid()) = user_id) lets each user read only their own. Write separate policies for insert, update and delete, with a with check clause on insert and update so nobody can write a row carrying someone else's user_id. Wrapping auth.uid() in a select lets Postgres evaluate it once per statement instead of per row, and an index on user_id keeps policies fast.

The gaps that catch people

Gap

Why it leaks

Fix

Views

Run with their creator's privileges by default, bypassing the tables' policies

Create them with security_invoker = true (Postgres 15+), or keep them out of exposed schemas

SECURITY DEFINER functions

Run as their owner, ignoring the caller's policies

Check the caller inside the function, or use SECURITY INVOKER

Storage buckets

Files have their own policies, separate from tables

Write storage policies per bucket; keep buckets private unless files are truly public

Policies that only check "signed in"

Every user reads every other user's rows

Compare against the row's owner or organization

A secret key in a server function used for everything

Requests without a user token bypass RLS entirely

Pass the user's token through so policies apply; keep the secret key for admin work

The ten-minute test

  1. Create two test accounts, A and B, and some data as A.

  2. As B, from the browser console or a script using the publishable key and B's token, try to select, update and delete A's rows directly through the API.

  3. Repeat with no token at all.

  4. As B, try to insert a row with A's user_id.

  5. Anything that succeeds is a leak. Fix it and keep the script as a test.

hoolco Monthly Review

Once a month: what changed in tech that month and what it means for you, from cloud and AI to security and the tools you build with. Every item links to its source. No sales pitch, unsubscribe in one click.

As of October 26, 2026. Provided as is, for general information, and accurate to the best of our knowledge on the date shown. Not legal, compliance, tax, financial or other professional advice. Prices, limits and vendor services change; check current vendor documentation before relying on anything here. Terms of use.