Built and run by one person.

Is enabling row-level security enough to protect my tables?

No - and this is the trap. Enabling RLS makes people feel done. The policy shape is what actually decides whether you are safe.

Why RLS matters at all. If a platform like Supabase publishes a PostgREST API over your database, your anon key is visible in frontend code. The key is not a secret and was never meant to be. RLS is your access control - without it, anyone holding that key can INSERT, UPDATE and DELETE any row in any table.

Now the part that catches people. A reasonable-looking policy - "users can update their own profile", USING (auth.uid() = user_id) - grants UPDATE on every column of the matching row. RLS decides which rows, not which columns. So if that table also holds is_admin, status, subscription_tier or usage limits, the user can update those on their own row. One API call, and they are an admin on a paid tier with no limits - with RLS switched on and working exactly as written.

The structural fix: keep sensitive columns - roles, subscription status, permissions, usage limits - in separate tables users can never write to. Billing webhooks and server-side logic write there with a service role. If you cannot restructure the schema, add a BEFORE UPDATE trigger that rejects changes to the protected columns.

And the simplest case people miss: if a table is backend-only and no frontend needs it, do not write a clever policy - revoke access from the anonymous role entirely. For read-only data, allow SELECT and nothing else.

The companion trap is worse, because it ignores your policies completely: SECURITY DEFINER functions. Full item with the SQL: https://www.tigzig.com/security/database.

← All Agents FAQ