This is what a RowProof report looks like. It was produced on the public test database, not on a client's app, so every name, row and number below comes from that database and from one recorded run.
Not a client. Test database:
rls-audit-fixture.sql. Script:
rls-audit.sql. Run on 2026-10-13, PostgreSQL 16.15, in a disposable local database. Scope: what a Standard review covers for authorization (script plus two-account tests). Billing and AI cost checks from Advanced are not part of this sample.
Worked findings
HIGH C05 Any signed-in user can read every order
public.orders :: "logged in reads"
Script
The SELECT policy only checks that the caller is signed in: (auth.uid() IS NOT NULL).
Evidence: user B asks for user A's orders
-- role authenticated, auth.uid() = user B
select id, user_id, total from public.orders where user_id = '<user A>';
id | user_id | total
1 | 00000000-0000-0000-0000-00000000000a | 100
2 | 00000000-0000-0000-0000-00000000000a | 250
3 | 00000000-0000-0000-0000-00000000000a | 75
(3 rows)
Impact
Any customer who creates an account can download every other customer's order history, including amounts. No hacking skill is needed: the same request your app already makes, with a different filter.
Fix
alter policy "logged in reads" on public.orders
using ((select auth.uid()) = user_id);
Retest: same request as user B
select count(*) from public.orders where user_id = '<user A>';
rows_of_user_a_visible_to_b
0
select id, user_id, total from public.orders;
4 | 00000000-0000-0000-0000-00000000000b | 40 -- only B's own order
HIGH C16 Users can upgrade their own plan
public.profiles :: profiles_update
Script
The UPDATE policy lets users edit their own row, and the authenticated role can write the column plan.
Evidence: user B edits their own plan
-- role authenticated, auth.uid() = user B
update public.profiles set plan = 'pro' where id = '<user B>' returning id, plan;
00000000-0000-0000-0000-00000000000b | pro
UPDATE 1
Impact
A free user can switch themselves to the paid plan from the browser console, without paying. The row-level policy is correct (users only touch their own row); the problem is which columns they may change.
Fix
revoke update on public.profiles from authenticated;
grant update (email) on public.profiles to authenticated;
-- grant only the columns your profile form edits; set plan from the server (for example a Stripe webhook)
Retest
update public.profiles set plan = 'pro' where id = '<user B>';
ERROR: permission denied for table profiles
update public.profiles set email = 'b2@example.test' where id = '<user B>';
UPDATE 1 -- the editable column still works
HIGH C13 Users can post in someone else's name
public.comments :: comments_insert
Script
The INSERT policy checks only (length(body) > 0) and never compares user_id to the caller, while authenticated can write user_id.
Evidence: user B inserts a comment owned by user A
-- role authenticated, auth.uid() = user B
insert into public.comments (id, user_id, body)
values (1, '<user A>', 'written by B in the name of A');
INSERT 0 1
-- stored row: user_id = 00000000-0000-0000-0000-00000000000a
Impact
Anyone signed in can create content that your app will show as written by another user. On a marketplace or a review section that is impersonation.
Fix
alter policy comments_insert on public.comments
with check ((select auth.uid()) = user_id and length(body) > 0);
Retest
insert into public.comments (id, user_id, body) values (2, '<user A>', '...');
ERROR: new row violates row-level security policy for table "comments"
insert into public.comments (id, user_id, body) values (3, '<user B>', 'my own comment');
INSERT 0 1 -- B can still post as B
HIGH C07 The C05 fix is not enough on its own
public.v_orders
After the C05 fix, an anonymous visitor (role anon, not signed in) still reads order data through the view, because the view runs with its owner's privileges and skips Row Level Security:
select count(*), sum(total) from public.v_orders; -- role anon
4 | 465
Fix, not applied in this sample: alter view public.v_orders set (security_invoker = true); or revoke the view from anon. This is why a report lists every path to the data, not only the first one found.