Guides / Brain

Row level security

Why every brain table should have row level security on, and how to exempt one on purpose.

Row level security (RLS) is a Postgres feature that blocks every read and write on a table unless a policy allows it. Supabase publishes the tables in your public schema through an automatic web API, and anyone holding the project's public anon key can call that API. A table without RLS can be read and changed by anyone with that key.

The brain therefore expects RLS on every table in the public schema, including tables it did not create. This guide shows how to find tables without it, how to turn it on, and how to exempt a table on purpose.

Time
10 minutes
For
Anyone running a brain on Postgres or Supabase

Before you start

  • A brain on Postgres, created with nexusbrain init --supabase or nexusbrain init --url.
  • A way to run SQL against the database, such as psql or the Supabase SQL editor, for the exemption steps.
  • A database role with the BYPASSRLS privilege (such as postgres) for applying the fixes.

01Why the brain requires RLS

The brain is meant to connect as a role with the BYPASSRLS privilege, so turning RLS on does not block the brain itself. What it blocks is the public web API: with RLS on and no policies, the anon key gets nothing.

On the local engine (PGLite) there is no web API and only one user, so the check is skipped and reports [OK] rls: Skipped.

  • nexusbrain doctor checks every base table in the public schema, not a fixed list.
  • Brain migrations enable RLS on existing tables and install an event trigger, auto_rls_on_create_table, that turns RLS on for every new table created in public.
  • The trigger and backfill leave other schemas, such as the ones Supabase manages, untouched.

02Check your tables

  1. Run the health check.

    Do not add --fast, which skips all database checks.

    nexusbrain doctor

    You should seeOn a protected brain: [OK] rls: RLS enabled on 42/42 public tables, with your own counts.

  2. Read the result if a table is missing RLS.

    Gaps show as [FAIL] rls: 2 table(s) WITHOUT Row Level Security: my_table, other_table. followed by Fix: and a ready-to-run ALTER TABLE statement for each table.

    Any failed check makes nexusbrain doctor exit with code 1, so a CI job or script that runs it will stop.

    You should seeYou have the list of unprotected tables.

03Turn RLS on

If the gaps are brain tables on an older install, applying pending migrations fixes them. For any table, you can also run the statements doctor printed.

  1. Apply pending brain migrations.

    Add --dry-run first if you want to see the plan without changing anything.

    nexusbrain apply-migrations --yes

    You should seeThe migrations run. Existing public tables without RLS and without an exemption comment get RLS turned on.

  2. Run any remaining fix statements from doctor.

    Copy the ALTER TABLE lines from the Fix: part of the doctor message and run them as a role with BYPASSRLS.

    ALTER TABLE "public"."my_table" ENABLE ROW LEVEL SECURITY;

    You should seePostgres answers ALTER TABLE.

  3. Check again.

    nexusbrain doctor

    You should see[OK] rls: RLS enabled on all public tables.

Warning

With RLS on and no policies, the anon key can no longer read the table. If another app reads that table through the Supabase API, give it a policy, or exempt the table as shown below.

04Exempt a table on purpose

Some tables are meant to be public, such as analytics a dashboard reads with the anon key. You can mark such a table as exempt with a Postgres comment. There is deliberately no brain command for this: it has to be written in SQL, where it is visible in schema diffs and survives pg_dump.

The comment must start with NEXUS_BRAIN:RLS_EXEMPT, then a space, then reason= and a reason of at least 4 characters that does not start with a space.

  1. Add the exemption comment to the table.

    COMMENT ON TABLE public.my_table IS 'NEXUS_BRAIN:RLS_EXEMPT reason=public dashboard reads this with the anon key';

    You should seePostgres answers COMMENT.

  2. Turn RLS off for that table, if it is on.

    The exemption only matters for a table whose RLS is off. New tables get RLS on automatically, so you usually need this step.

    ALTER TABLE public.my_table DISABLE ROW LEVEL SECURITY;

    You should seePostgres answers ALTER TABLE.

  3. Confirm doctor lists the exemption.

    nexusbrain doctor

    You should seeThe rls check is [OK] and ends with (1 explicitly exempt: my_table). Doctor lists exempt tables on every run so they never go unnoticed.

Warning

Add exemption comments before you upgrade or run nexusbrain apply-migrations. The migration backfill turns RLS on for every public table that does not carry a valid comment, including tables you left open on purpose.

05Remove an exemption

  1. Remove the comment.

    COMMENT ON TABLE public.my_table IS NULL;

    You should seePostgres answers COMMENT.

  2. Turn RLS back on.

    ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;

    You should seenexusbrain doctor counts the table as protected and no longer lists it as exempt.

06Troubleshooting

Doctor still fails for a table that has a NEXUS_BRAIN:RLS_EXEMPT comment.

The comment must include reason= followed by at least 4 characters. A bare NEXUS_BRAIN:RLS_EXEMPT does not count.

The comment must be at the very start, with no text before it.

A migration fails with does not have BYPASSRLS privilege or does not have BYPASSRLS.

The brain is connected as a role that cannot safely change RLS. Re-run nexusbrain apply-migrations --yes with a connection that uses postgres or another role with BYPASSRLS.

[WARN] rls: Could not check RLS status

Doctor could not read the table list. Check that the database is reachable and that the brain role can read pg_tables, then run nexusbrain doctor again.

Another app sharing the Supabase project stopped reading a table after the upgrade.

The migration turned RLS on for that table. Add a Postgres policy for the access the app needs, or exempt the table with a comment and turn RLS off for it.