DEV Community

MD Mehedi Hasan
MD Mehedi Hasan

Posted on

Designing multi-tenant Supabase Row Level Security for an AI PRD generator

Multi-tenancy bugs rarely look like bugs. The query works. The page loads. The data is just… somebody else's.

When I built Web Dev Mehedi — a tool that turns an app idea into a PRD, a database schema, API contracts and coding-agent rules — the generator had to produce Supabase Row Level Security (RLS) policies for every table it invented, for apps it had never seen. That forced me to stop treating RLS as a per-project craft and turn it into a small set of rules a generator can apply reliably. Those rules turned out to be good rules for humans too.

Rule 1: Every tenant-owned table carries its owner

In generated schemas, any table that belongs to a user or a workspace has a non-nullable ownership column (owner_id or workspace_id) referencing the parent, with on delete cascade. No ownership column, no RLS — so the ownership column is not optional decoration; it is the thing the policy will test.

create table public.products (
  id uuid primary key default gen_random_uuid(),
  shop_id uuid not null references public.shops(id) on delete cascade,
  name text not null,
  stock integer not null default 0 check (stock >= 0),
  created_at timestamptz not null default now()
);
Enter fullscreen mode Exit fullscreen mode

Rule 2: Policies are written the same hour as the table

A table without policies, on a project where RLS is enabled, fails closed — nothing is readable. Teams experience that as "Supabase is broken," disable RLS "temporarily," and ship a data leak. In the generator, create table and create policy are emitted in the same block, always. For a human team the equivalent rule is: the migration that creates the table contains its policies, or the PR doesn't merge.

alter table public.products enable row level security;

create policy "shop members manage their products"
  on public.products
  for all
  using (
    exists (
      select 1 from public.shop_members m
      where m.shop_id = products.shop_id
        and m.user_id = auth.uid()
    )
  );
Enter fullscreen mode Exit fullscreen mode

Rule 3: Membership beats ownership for anything shared

The naive policy tests shop_id in (select id from shops where owner = auth.uid()). That breaks the moment a second person joins the shop — a staff member is not the owner, so they see nothing, and someone "fixes" it by widening the policy. A membership table (shop_members) as the single source of truth survives owners, staff and future roles, because the policy asks one question: is this user a member of the tenant this row belongs to?

Rule 4: One policy per operation when behaviour differs

for all is fine when reads and writes follow the same rule — it keeps generated SQL short and auditable. The moment they differ (anyone in the shop can read, only admins can delete), split into per-operation policies. Four explicit policies are easier to review than one clever one, and RLS review is where "clever" goes to die.

Rule 5: Never bypass RLS from application code to "make it work"

The service-role key bypasses RLS entirely. In generated API contracts, it is reserved for genuinely system-level jobs (billing webhooks, scheduled cleanup) and is explicitly forbidden in user-facing endpoints. When a query returns nothing, the correct debugging order is: (1) is RLS enabled on the table, (2) does a policy cover this operation, (3) does the membership row exist. "Use the service key" is not step 4.

The checklist the generator applies — steal it

  1. Tenant table → ownership/membership column present, non-null, indexed.
  2. RLS enabled in the same migration as the table.
  3. A policy exists for every operation the API contract uses.
  4. Policies test membership, not ad-hoc conditions copied from the app layer.
  5. No user-facing endpoint uses the service-role key.

None of this is exotic. That's the point: multi-tenant safety mostly comes from applying boring rules every single time, which is exactly the kind of thing a generator — or a checklist taped to your PR template — is better at than memory.

If you want to see these rules applied to your own app idea, the generator is live: describe the product, and the schema comes back with the policies already written. Web Dev Mehedi — 50 free credits, no card.

Top comments (0)