Skip to content
RowShield
All posts

The authorization posture of vibe-coded apps

What AI assistance systematically gets right and wrong about Supabase authorization - observed failure patterns, with an ordered hardening sequence.

RowShield12 min read

AI-built apps ship fast because the model produces plausible everything - including plausible security. This article catalogs what assistance systematically gets right and wrong about authorization, states the patterns without dramatizing them, and gives founders an ordered hardening sequence.

A generation of apps is being built conversationally: describe the product, let an assistant generate schema, policies, and client code, iterate until it works. The result works — that's the striking part. Features function, data flows, launches happen. But "works" is a functional verdict, and authorization is not a functional property. It is defined by what happens when someone does something the demo never tried, which is precisely the territory generative development doesn't naturally explore.

This article is about the gap between plausible security and actual security in AI-assisted Supabase projects. Everything here is pattern-level observation — shapes that recur across projects regardless of which assistant generated them — because the failure modes come from the task (generating authorization code from feature descriptions), not from any particular model having bad intentions.

What AI assistance actually changes about authorization

Traditional development had a structural safety net: somebody had to understand the policy they wrote. Even badly written policies passed through a mind that could, in principle, ask "who else can see this row?" Conversational development dissolves that contact point. The policy appears in a migration file, correct-looking and well-named, authored by nobody in particular.

Three properties of the new workflow shape its failure modes:

Authorization arrives as a side effect. When you describe a feature ("users can save their favorite recipes"), the assistant generates whatever database machinery makes the demo work — sometimes including RLS policies, sometimes not, depending on phrasing and context. Whether a given table got protection becomes an accident of conversation history rather than a decision anyone made.

Each response optimizes locally. A model answering "the recipe page shows empty for other users, fix it" will produce the minimal change that fixes your symptom — often widening a policy — without visibility into whether that width breaks your isolation model globally. Policies combine with OR semantics, so local fixes have global consequences that no single response can see.

Plausibility is indistinguishable from correctness in review-by-skim. Generated policies use the right keywords, idiomatic naming (*_select_own), even comments. Reviewing them feels like reviewing security. But tautologies and missing clauses look exactly like careful ones at skim speed — the difference lives in one expression, and skimming skips expressions.

None of this makes AI-assisted development wrong. It makes its output a specific kind of input: code with above-average plausibility and unverified guarantees, which happens to be the exact description of what authorization review exists for.

Patterns we observe

Across AI-generated Supabase backends, the same shapes recur. None of these are hypothetical — each corresponds to a detection rule or manual check this site maintains, because each recurs enough to automate:

PatternWhat it looks likeConsequence
Placeholder tautologyusing (true) left from debugging, or a policy named "temp"Table readable/writable by every authenticated user
Half-written writesInsert/update policies with no WITH CHECKRows planted into other users' scopes
Per-feature driftNew tables from later prompts lack the RLS lines earlier ones gotOpen windows proportional to project age
Server-key shortcutsEdge functions using service credentials for user requestsPolicy layer bypassed on hot paths
Default storageBuckets created alongside features with default visibilityObjects served without auth checks
Missing negative testsSuite proves features work; nothing probes denialAny of the above survive indefinitely

Two of these deserve expansion because they interact: per-feature drift and missing negative tests compound. Drift introduces unprotected surfaces continuously; absent denial-tests mean nothing notices. A team can run for months in that state with green CI, working demos, and a public API that returns other users' rows. The flagship drift walkthrough narrates exactly this compounding over six months of ordinary changes.

The first pattern — the placeholder tautology — earns a closer look because of how innocently it's born. A developer describes a feature; the generated code includes:

create policy "notes_select"
  on notes for select
  to authenticated
  using (true);  -- TODO: restrict by user

Sometimes the TODO survives. Sometimes there's no TODO and no comment — just using (true), which reads like a decision rather than a placeholder. Either way, every authenticated user can read every row, and the policy's professional name and placement make it invisible during review. The mechanics of why this passes functional testing are covered in the tautology rule page; the relevant point here is generational: an assistant asked to "add a select policy so the page works" has fulfilled the request exactly. The security property was never in the prompt, so it never entered the code.

Its sibling arrives when writes join reads later:

create policy "notes_insert"
  on notes for insert
  to authenticated
  with check (true);

Now any signed-in user can create notes attributed to any owner_id they supply. Combined with the tautological select, users can even see what they planted into others' accounts — but plenty of these tables keep stricter selects while accepting anything on insert, which produces the quieter variant: planted rows their victims can't see until application behavior betrays them.

Why generated policies look finished

The most dangerous generated artifacts aren't obviously lazy. They carry structure: consistent naming conventions, role targeting (to authenticated), even the (select auth.uid()) performance wrapper that good documentation recommends. This surface correctness is genuine — assistants learn from the same official documentation this site cites — which is what makes skim-review fail.

The deficits live deeper than syntax:

Scope blindness. A policy generated for one prompt cannot know what other prompts created. Only a catalog-level view reveals that recipes got a careful ownership policy while the recipe_tags table added two prompts later got nothing.

Intent inversion under pressure. When the developer says "it errors when I insert," the helpful fix targets the error. If the error was a correctly-functioning WITH CHECK rejecting a malformed payload, the generated fix may relax the check instead — converting a security control into a bug report resolution.

Test-shaped reasoning. Assistants verify logic against the cases implied in conversation: the happy path, the logged-in user, the visible page. Authorization defects live in adversarial cases — the forged owner_id, the guessed UUID — that nobody mentions because nobody is attacking yet.

The practical conclusion isn't "review everything harder" — vague advice dies in practice. It's that review needs targets, and the patterns above are the target list. Each one is mechanically detectable, which means both human reviewers and tools can check for them without judgment calls.

One reframe helps teams adopt this posture without resentment toward their tooling: the same properties that create these gaps also make the fixes fast. Because the patterns are consistent, a single catalog query surfaces every table needing attention; because generated code follows conventions, corrected policies merge cleanly; because the assistant applies feedback literally, review comments translate directly into fixed statements. The workflow that produced the exposure is, with one addition — verification gates — the same workflow that eliminates it.

The hardening sequence

If your app was built conversationally and you're not sure what its authorization looks like today, work through this order. Each step uses mechanics documented elsewhere on this site; the sequence is what matters — later steps assume earlier ones, and the whole list is designed so that measurement precedes modification and nothing requires a pause in shipping.

1. Inventory before you improve. List every table with its RLS flag and policy count:

select c.relname,
       c.relrowsecurity as rls_enabled,
       count(p.policyname) as policy_count
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
left join pg_policies p
       on p.schemaname = n.nspname and p.tablename = c.relname
where n.nspname = 'public' and c.relkind = 'r'
group by c.relname, c.relrowsecurity
order by c.relname;

Every row with rls_enabled = false is an open window right now (closing that first); every zero-policy-but-enabled row is a locked room waiting for its rules.

2. Read the union, not the lines. For each protected table, dump its policies and read all same-command policies as one OR expression — the method from policy sprawl. Flag any true, any null with_check on a write path, and any policy comparing a column against something other than (select auth.uid()).

3. Probe as an outsider. With only your anon key, request each exposed table. Empty arrays pass; returned rows fail. This single check catches the entire inventory-step residue plus anything migrations bypassed, and it is literally what our free scan automates.

4. Prove isolation between two accounts. Create two test users and run the cross-access checks — reads, forged writes, ownership transfers — from the tenant-isolation test. Failures here name their own fix.

5. Contain server power. Grep built assets for service-grade key material and inventory every service-role call site, per service-role leakage.

6. Add the denial tests. Convert every fix from steps 2–5 into an assertion in your test suite, so the next conversational iteration inherits guardrails rather than starting from zero. The pgTAP pattern in our testing guide fits existing suites without ceremony.

Steps 1–3 typically take an afternoon. Teams routinely find that step 1 alone — seeing the flag column next to every table they've shipped — reorders their priorities more effectively than any amount of general advice.

A worked retrofit: from open to owned

To make the sequence concrete, here is the most common single fix in full: converting an unprotected, AI-generated table into a properly scoped one without breaking the feature it powers. Before state — the table works, nothing protects it:

create table notes (
  id uuid primary key default gen_random_uuid(),
  owner_id uuid not null,
  body text not null,
  created_at timestamptz not null default now()
);

After state, in one migration:

alter table notes enable row level security;

create policy "notes_select_own"
  on notes for select
  to authenticated
  using ((select auth.uid()) = owner_id);

create policy "notes_insert_own"
  on notes for insert
  to authenticated
  with check ((select auth.uid()) = owner_id);

create policy "notes_update_own"
  on notes for update
  to authenticated
  using ((select auth.uid()) = owner_id)
  with check ((select auth.uid()) = owner_id);

create policy "notes_delete_own"
  on notes for delete
  to authenticated
  using ((select auth.uid()) = owner_id);

create index notes_owner_id_idx on notes (owner_id);

Read what changed in terms of guarantees: reads now return only rows whose owner_id matches the caller's verified identity; inserts are only accepted when the resulting row belongs to the writer; updates constrain both the target and the result, closing the ownership-transfer hole; deletes match only owned rows. The added index keeps every one of those predicates fast as the table grows. This is the complete pattern — four policies plus an index — and it transfers to nearly any user-owned table by renaming columns. Tables with richer sharing models add a membership branch, exactly as the multi-tenant patterns comparison on this site shows.

Verification closes the loop: run the two-account probes from the tenant-isolation playbook against the retrofitted table and watch all five checks flip to passing. One migration, five green outcomes.

Where humans stay in the loop

Hardening is not a rejection of AI-assisted development; it's the complement that makes it shippable. The division of labor that works: generate freely, then gate on checks that don't require judgment. Humans answer intent questions — should editors see invoices marked draft? — while machines verify conformance questions — does every table carry RLS, does every write carry a check, does the public surface return empty?

That second list is exactly what RowShield automates, because we watched the same patterns accumulate across projects until checking them by hand became the bottleneck. The intent questions remain yours regardless of tooling; no scan can know that drafts were meant to be editor-visible. But every conformance question has a yes/no answer, and answers beat vibes — in authorization as anywhere else — because answers can be re-checked automatically whenever the ground shifts under them.

The posture worth adopting as a founder or team lead: treat generated authorization code like generated crypto. Not banned — just never trusted on plausibility, always tested against adversarial cases, and monitored for drift after merge. The apps shipping this way aren't less secure than hand-built ones; they're differently unverified, and verification has never been cheaper.

Common questions

Is this specific to one AI coding tool?

No — the patterns track the workflow, not the vendor. Any assistant generating policies from feature descriptions produces the same shapes, for the reasons in the section above: local optimization, scope blindness, and test-shaped reasoning are properties of the task.

We asked the assistant to write secure policies. Isn't that enough?

It raises the baseline — you'll get to authenticated clauses and sensible naming. What it can't produce is coverage knowledge (what did previous prompts create?), adversarial testing (who else can read this?), or drift monitoring (what changed since last week?). Those are process properties, not prompt properties.

How do I explain this state to non-technical stakeholders?

One sentence: the app does everything we asked it to; nobody has yet checked what it allows that we didn't ask for. Then show the inventory query's output — flags and counts land with non-engineers more effectively than any policy explanation.

Can I retrofit security without pausing feature work?

Yes, and the sequence is ordered to allow exactly that. Steps 1–3 are measurement, not modification; step 4's failures triage themselves by severity; and the fixes concentrate in a handful of migration files. Hardening interleaves with ongoing work comfortably once the inventory exists — the exception being live exposures (open windows, leaked keys), which jump the queue on any reasonable priority list.

Should I stop using AI assistance for authorization code entirely?

That overcorrects. Assistants produce solid drafts — complete policy sets in seconds that would take a human an hour to type — and they apply review feedback reliably: "add WITH CHECK matching the USING clause" produces exactly that. What changes is who owns verification: every generated statement enters the same gauntlet as human-written code — catalog checks, two-account probes, denial tests in CI. Teams running that gauntlet accept generated drafts happily; teams skipping it shouldn't accept anyone's drafts.


See your project's current posture in minutes: run the free scan — paste your app URL and get findings mapped to the patterns above, with remediation SQL proposed for review.

RowShield is an independent product and is not affiliated with, endorsed by, or sponsored by Supabase, Inc.

RowShield checks what a deployed Supabase app exposes: a free anonymous, read-only probe, and scheduled policy-metadata and drift checks for connected projects. Run a free audit.

RowShield is a Veristria product. More about RowShield.