Skip to content
OTFotf
All posts

Row-level security production checklist: test every path before launch

D
DaveAuthor
9 min read
Row-level security production checklist: test every path before launch

A row-level security production checklist should prove more than “RLS is enabled.” A production database needs the right grants, policies for each operation, tests for allowed and denied paths, safe handling of views and functions, and a review process that catches a missing tenant boundary before launch.

Supabase’s Row Level Security guide makes the key point clearly: grants and policies work together. A policy narrows which rows an operation can affect, but adding a policy does not remove an overly broad table grant. Start with the access model, then verify the database behavior with a role that matches the application request.

Write the access model before SQL

For each exposed table, write a small matrix:

OperationRole or actorScopeExpected result
Selectsigned-in membercurrent workspaceallowed rows only
Insertsigned-in membercurrent workspaceallowed with server-owned owner ID
Updateworkspace editorcurrent workspaceeditable columns only
Deleteworkspace ownercurrent workspaceallowed and audited
Any operationsigned-out visitorprivate workspacedenied

Do not start with a policy copied from another table. Decide whether access is based on the current user, workspace membership, ownership, role, or a combination. Identify which values come from the authenticated request and which must be read from the database.

A row policy should answer one question: may this role perform this operation on this row? Keep business workflow checks separate when they need a transaction, approval, or audit record.

Enable RLS on every exposed table

Supabase’s guide warns that a table in an exposed schema without RLS can be readable and writable by roles that have grants on it. Treat every new exposed table as incomplete until it has an explicit RLS decision.

A migration should make the decision visible:

alter table public.projects enable row level security;

create policy "members can read projects in their workspace"
on public.projects
for select
to authenticated
using (
  workspace_id in (
    select workspace_id
    from public.workspace_members
    where user_id = (select auth.uid())
  )
);

The table and column names above are illustrative. Adapt them to your schema and verify the membership relationship before deploying. The important properties are explicit RLS, an explicit role, and a scope derived from the authenticated identity rather than a client-supplied workspace identifier.

Do not assume that enabling RLS automatically makes every request safe. Check table grants, policies, views, RPC functions, storage access, and service-role usage as separate surfaces.

11 production screens. Login, database, payments — all wired.

The SaaS Dashboard Kit ships everything already connected. Nothing to set up. Live demo at saas.otf-kit.dev.

See the live demo

Check grants as well as policies

Supabase documents two database checks before a client touches a table: grants decide whether a role can run the operation, and policies decide which rows the operation applies to. Review both.

For each exposed table, record:

  • Which operations anon needs, if any.
  • Which operations authenticated needs.
  • Which operations are server-only.
  • Whether service_role is used outside a trusted server.
  • Whether a migration changes default privileges.
  • Whether a new table inherits broader access than intended.

A private application often needs no anonymous access to business tables. Do not leave an anon grant in place because a policy happens to return no rows today. A future policy edit, view, or function can change the outcome.

Keep the service role server-side. Supabase notes that the service role bypasses RLS, so it must not be exposed in browser code, client configuration, logs, or user-controlled jobs. If a background worker needs elevated access, give it a narrow job contract and audit the operation rather than passing a general-purpose credential through the request.

Write policies per operation

A read rule is not an insert rule. A user who may select a row may not be allowed to update or delete it. For inserts, use a with check condition to validate the proposed row. For updates, reason about both the row being found and the row after the change.

create policy "members can create projects in their workspace"
on public.projects
for insert
to authenticated
with check (
  workspace_id in (
    select workspace_id
    from public.workspace_members
    where user_id = (select auth.uid())
      and role in ('owner', 'editor')
  )
);

create policy "editors can update projects"
on public.projects
for update
to authenticated
using (
  workspace_id in (
    select workspace_id
    from public.workspace_members
    where user_id = (select auth.uid())
      and role in ('owner', 'editor')
  )
)
with check (
  workspace_id in (
    select workspace_id
    from public.workspace_members
    where user_id = (select auth.uid())
      and role in ('owner', 'editor')
  )
);

The role names and membership table are examples, not claims about your project. Test that an update cannot move a row into another workspace by changing workspace_id. If the policy checks only the original row, the new row may still be unsafe.

For columns that users must not control—owner IDs, workspace IDs, billing state, audit timestamps—set values in a trusted server path or use database constraints and triggers where appropriate. A hidden input is not an authorization control.

Test as the application roles

Policy tests should cover both positive and negative cases. Run them with the database roles and authentication claims your application actually uses. A test run as an administrator can pass while the browser role remains broken or over-permitted.

Minimum cases for a workspace table:

- member reads a row in their workspace: allowed
- member reads a row in another workspace: denied or invisible
- signed-out request reads a private row: denied
- editor inserts into their workspace: allowed
- editor inserts into another workspace: denied
- member updates a protected ownership field: denied
- owner deletes an allowed row: allowed
- member deletes an owner-only row: denied
- service worker performs its one server task: allowed and audited

Test empty results carefully. A query that returns zero rows can mean “no matching data,” “not authorized,” or “policy expression failed to match.” Your application should not turn a permission failure into a misleading empty state when that distinction matters.

Use a disposable test database or a controlled fixture set. Include two workspaces with similar records so a missing scope is visible. Add a test that attempts to change the workspace identifier on an existing record.

Inspect views, functions, and storage

RLS on a base table does not settle every access path. Supabase’s documentation notes that views can bypass RLS by default because of how they are created. Review every view exposed to the client and verify its security behavior.

Also inspect:

  • Database functions and their execution privileges.
  • RPC endpoints and argument validation.
  • Storage buckets and object policies.
  • Foreign tables or external data sources.
  • Materialized views and refresh jobs.
  • Admin dashboards using elevated credentials.

A safe table can be undermined by a view that selects every row, an RPC function that accepts an arbitrary workspace ID, or a storage object path that is predictable. Trace the user’s request all the way to the underlying data.

If a function needs elevated privileges, keep its inputs narrow, validate ownership inside the function, and document why it cannot run with the caller’s privileges. Prefer a small purpose-built operation over a general query endpoint.

Review policy performance

A correct policy can still cause slow queries when it repeatedly evaluates a membership lookup across a large table. Check the query plan and add indexes for the columns used in scope checks and joins. Do not remove the policy to fix a performance issue; change the data access pattern while preserving the boundary.

Supabase Advisors provide deterministic security and performance findings. The Advisors documentation describes checks for issues such as incorrectly configured RLS policies and unindexed foreign keys, and points to Studio, MCP, CLI, and API access. Treat an advisor result as a finding to investigate, not as proof that the full application is secure.

Run advisor checks after migrations and during release review. Compare findings with query logs and the intended access model. A clean advisor result cannot detect every business authorization mistake.

Keep migrations reversible and reviewable

Store RLS changes in version-controlled migrations. Name policies after the actor and operation, not after an implementation detail that may become unclear later. Avoid a migration that silently drops a policy without adding its replacement in the same reviewed change.

A release checklist should show:

Migration:
- RLS enabled on new and changed exposed tables
- Grants reviewed for anon and authenticated
- Select, insert, update, delete policies reviewed separately
- Views, functions, and storage paths checked
- Positive and negative policy tests passed
- Advisor findings reviewed
- Rollback or forward-fix path documented

Test the migration against a copy of realistic fixtures. Confirm that old application versions can coexist during a rolling deployment if the schema change requires it. If a new policy depends on a column that is not present in the previous version, plan the order of application and database changes.

Give AI coding agents a bounded task

AI coding agents can write policy SQL quickly, but authorization is a poor place to accept an unreviewed guess. Give the agent the table schema, intended access matrix, existing membership helper, required tests, and prohibited changes.

Add RLS for the reports table.

- Read the existing membership and role conventions first.
- Implement select, insert, update, and delete policies separately.
- Do not change grants or service-role configuration without listing the impact.
- Add tests for same-workspace access, cross-workspace denial, and ownership changes.
- Inspect views and RPC functions that expose reports.
- Return the migration, test output, and any assumption that remains.

Review the generated SQL as a security change. Ask what happens when a user loses membership during a session, when a row changes workspace, and when a service job retries. The agent can speed up the draft; the access decision remains yours.

Ship an AI MVP to production covers the wider release gate. Agent-readable repository structure covers the project context an agent needs before editing policy files.

Connect the database boundary to the product foundation

OTF’s templates page verifies a free MIT component SDK and a free AI configurations pack for Cursor, Claude, and Lovable. Those assets can provide a starting repository for an application, but they do not prove that a specific kit has your workspace schema or RLS policies. Verify the current kit scope and keep database authorization in your own reviewed migrations.

Row-level security is ready for production when the database, not just the UI, enforces the access model. Enable it on every exposed table, review grants, write policies per operation, test both allowed and denied paths, inspect views and functions, check advisor findings, and keep the migration reviewable. If an AI agent writes the first draft, make the acceptance checklist stricter, not looser.

Sources

supabasebackendagents
OTF SaaS Dashboard Kit

Ship the product, not the setup.

  • 11 production screens — auth, billing, team, analytics, settings
  • Real database, payments, and login — all wired on day 1
  • AI configs pre-tuned so your agent extends instead of regenerates