Book a consult
Services

Production SI, engineered end to end, six eval-gated service lines.

Visit Services →
Solutions

The same playbook, tuned to the constraints of the sectors we ship into most.

Visit Solutions →
Live Projects

Proof, not promises, selected case studies and recognition.

Visit Live Projects →
How we work

A transparent, 3-phase playbook from first audit to embedded team.

Visit How we work →
Studio

The senior team behind the work, and how to reach us.

Visit Studio →
octalcode
Services
Solutions
Live Projects
How we work
Studio
● OCTALCODE · INFINITE SOLUTIONS · ONE AGENCYBook a consult ↗

Engineering · 7 min · November 11, 2026

Supabase Row Level Security Checklist for Prompt-to-App Builds

A practical RLS review for Supabase apps generated by prompt-to-app builders: tables that skip RLS, one policy per operation, secret keys, views and functions, storage, testing as each role and the Security Advisor.

OCTALCODE JOURNALRLS ONPOLICY PER ACTIONTESTED PER ROLEEvery table,every role.

If your app was generated by a prompt-to-app builder on top of Supabase, there is often no custom API between your users and your data. The browser queries Postgres through Supabase's Data API with a publishable key that ships inside the app. Supabase describes that key as safe to expose, but only because it can reach nothing beyond what Row Level Security (RLS) allows. In this architecture, RLS is your access control layer.

Supabase's production security checklist puts it plainly: a table without RLS and reasonable policies lets any client read and change its data. This checklist is for whoever signs off on that layer before real users arrive, and every item comes from Supabase's own documentation.

Why are generated apps more exposed?

Generators write policies to make a feature work, and a policy that lets everyone through makes every feature work. Lovable's documentation says it sets up basic RLS policies automatically and that you should review them early. Watch for three patterns: tables created by SQL migrations with RLS never switched on, policies that amount to "allow all", and server code that uses a secret key and skips every policy.

How do I check Row Level Security in a Supabase app before launch?

Confirm RLS is enabled on every table in an exposed schema, including tables created by SQL migrations, which do not get it by default. Give each table a policy for every operation it needs, scoped to the authenticated role. Keep secret keys out of the browser, because they bypass every policy. Set security_invoker on views and lock down security definer functions. Then test each table as a signed-out visitor, two different users and an admin, and run the Security Advisor after every schema change.

1. Is RLS enabled on every exposed table?

Supabase enables RLS by default on tables created through the dashboard. Tables created in the SQL Editor or with other tools need it enabled explicitly, and generated apps create most tables through SQL migrations. Supabase also notes that a new table in the public schema can start with privileges granted to the anon, authenticated and service_role roles.

  • List public tables without RLS: select tablename from pg_tables where schemaname = 'public' and rowsecurity = false. The answer should be empty, or every row should have a written reason.
  • Add "alter table ... enable row level security" to every migration that creates a table, or use the event trigger approach Supabase documents to enable it automatically.
  • Keep internal tables and helper functions in a schema the Data API does not expose. Supabase suggests a dedicated api schema so the exposed surface is easy to see.
  • If you use Realtime, note that RLS does not filter Postgres Changes DELETE events. Supabase recommends Broadcast with Realtime Authorization for sensitive changes.

2. Does each table have a policy for each operation it needs?

With RLS enabled and no policies, the Data API returns nothing to clients using the publishable key. That is safe, but it breaks features, and that breakage is usually when someone adds a policy with a condition of true to make the error go away. Write one policy per operation instead:

  • select and delete: a using clause that decides which existing rows the user can see or remove.
  • insert: a with check clause that decides what a new row may contain, for example that its user_id equals the caller's ID.
  • update: both clauses, using for which rows can be changed and with check for what the result may look like. Supabase also notes that an update needs a matching select policy.
  • delete: decide whether users should hard delete at all, or only mark rows as deleted.

Remember that RLS decides which rows, not which columns. If users may edit their profile but not the plan or role stored on it, move those fields to a table they cannot write to, or restrict them with column privileges.

3. Are policies written the way Supabase recommends?

  • Name the role. Add "to authenticated" so a policy for signed-in users is not evaluated for anonymous requests.
  • Handle the signed-out case. auth.uid() returns null when nobody is signed in, and null never equals a user_id. Supabase recommends an explicit "auth.uid() is not null" check so the intent is clear.
  • Wrap helper calls as (select auth.uid()). Postgres can then evaluate them once per statement instead of once per row.
  • Index every column your policies filter on. A column only counts as indexed when it leads a btree index, so a composite primary key on (team_id, user_id) does not index user_id.
  • Never authorize from user metadata. Users can change raw_user_meta_data themselves. Supabase recommends raw_app_meta_data, which users cannot change, or a table you control. JWT claims can also be stale until the token refreshes.

4. Where is the secret key used?

Supabase's secret keys (the legacy service_role key) bypass every RLS policy. They belong in backend code only: never in a browser, a shipped mobile app or source control. Supabase rejects secret keys sent from a browser, based on the User-Agent header, but its docs warn that an attacker can still use a leaked key from other tools.

  • Search the built front-end bundle and the git history for the secret key and the legacy service_role key.
  • List every Edge Function and server route that uses a secret key. Each one must check who is calling and what they may do, because the database will not. Lovable's docs make the same point about server code using the service role.
  • Where a function acts for a signed-in user, pass that user's access token so their policies apply, and keep the secret key for genuine admin jobs.
  • Use a separate secret key per backend component, so a leak means rotating one key, not all of them.
  • If an SI agent or LLM feature can query the database, give it the user's scope, not a secret key. Our prompt injection testing checklist covers why a model's tool access needs the same limits as a user.

5. Do views and functions go around your policies?

Supabase's docs note that views bypass RLS by default, because they are usually created by the postgres user. On Postgres 15 and later, set security_invoker = true on the view so it obeys the policies of the tables underneath. On older versions, revoke access from anon and authenticated, or move the view to a schema the API does not expose.

Security definer functions run with their creator's privileges and can skip RLS too. Generated apps often add one to fix a recursive policy, which is a legitimate pattern if it follows Supabase's rules: set search_path to an empty string, schema-qualify every name inside, and do not create the function in an exposed schema. Check who can execute it. The Security Advisor flags security definer functions that anon or authenticated users can run.

6. Are storage buckets covered?

Files live in storage.objects, which has its own policies. Supabase Storage does not allow uploads to a bucket without RLS policies: an upload needs an insert policy, and overwriting an existing file (upsert) also needs select and update. Public buckets are publicly accessible, so receipts, ID documents and contracts belong in a private bucket, with policies that tie each path to its owner or organization.

7. Have you tested every table as each role?

Reading policies is not enough. Test them as a signed-out visitor, user A, user B, a member of a different organization and an admin. Supabase documents a way to do this in the database with pgTAP tests, run with supabase test db.

  • Keep one test file per table with RLS enabled, asserting both allowed and denied cases for select, insert, update and delete, for anon and authenticated.
  • Impersonate users inside the test with set local role and the request.jwt.claim.sub setting, as Supabase's examples show.
  • Know what a denial looks like. A missing grant raises error 42501, but a using clause that filters a row out raises no error and simply affects zero rows, so assert that the targeted row is unchanged.
  • Prove the tests can fail: temporarily swap in a policy of using (true) and confirm the deny tests go red. Dropping the real policy proves nothing, because with no policy every read is denied anyway.
  • Add an end-to-end check from the browser with two real accounts, trying to read and edit each other's records.

If your app also searches documents for an LLM feature, the same principle applies to retrieval. We cover it in per-user access control in RAG.

8. Run the Security Advisor, and keep running it

Supabase's Security Advisor runs deterministic checks against your project, in the dashboard or with supabase db advisors from the CLI. Its lints include RLS disabled on public tables, RLS enabled with no policy, security definer views, functions with a mutable search_path, policies that reference user metadata, overly permissive policies and public buckets that allow listing. Some findings may be intentional, so check each against your access model. Run it after every schema change, ideally in CI.

The short version

  1. RLS enabled on every table in an exposed schema, including those created by migrations.
  2. A policy per operation, scoped to a role, with no blanket allow-all conditions.
  3. Authorization read from app metadata or your own tables, never user metadata.
  4. Secret keys only in backend code, with their own authorization checks.
  5. Views set to security_invoker, and security definer functions locked down.
  6. Private buckets with storage policies for anything sensitive.
  7. pgTAP tests for each table and role, plus a two-account browser test.
  8. A clean Security Advisor report, rerun after every schema change.

Passing this list is one exit criterion for moving a generated prototype into production, alongside the others in what each stage must prove.

Where Octalcode fits

Octalcode is a studio in Lahore, with a US presence in Austin, whose custom software team reviews and hardens apps started in prompt-to-app builders, including Supabase policies, functions and tests like the ones above.

If you would like a second reviewer on your policies before launch, get in touch. We sign an NDA before we see any code or data, keep access to the project team, and reply to enquiries within 24 hours.

Available for new engagements

Have a production SI problem to solve?

30 minutes, one of our seniors, no slide deck. By the end of the call you will know whether we are the right team, and if not, who is.

Or call +1 (512) 710-5701

Let's scope it.Instant answers · free project scoping