Can users give themselves your paid plan?
In a lot of Lovable, Bolt and Cursor apps, the "upgrade" step is a line like supabase.from("profiles").update({ is_pro: true }) running in the browser. If that works for your code, it works for anyone: open the console, run the same line, and the paid plan is theirs for free. The same goes for plan, credits, role and trial_ends_at.
Why it happens
The usual Row Level Security policy on a profiles table is "users can update their own row": using (auth.uid() = id). That policy decides which rows a user may change, not which columns. If billing columns live on that row, the user can change them too. Removing the update from your frontend code doesn't help on its own: the policy still allows it, and anyone can write the request by hand.
Test it on your own app (30 seconds)
- Log in to your app as a normal, free user.
- Open DevTools, Console, and run:
await supabase.from("profiles").update({ is_pro: true }).eq("id", (await supabase.auth.getUser()).data.user.id)(use your own table and column names; ifsupabaseisn't defined, find the client in your bundle or test with a REST call). - Reload. If you're now pro, so is anyone who tries it.
Only run this on apps you own.
Fix 1: only let users update the columns they should
Postgres can grant updates per column. RLS still decides the rows; the grant decides the columns.
revoke update on table public.profiles from anon, authenticated;
grant update (full_name, avatar_url) on table public.profiles to authenticated;
List every column users are allowed to edit. If users insert their own profile row, do the same for insert, so nobody can sign up with is_pro = true.
Fix 2: block billing changes with a trigger
If you'd rather keep the table grants as they are, a trigger can refuse changes to billing columns unless they come from your server:
create or replace function public.protect_billing_columns()
returns trigger language plpgsql as $$
begin
if (new.is_pro is distinct from old.is_pro or new.plan is distinct from old.plan)
and coalesce(auth.role(), '') <> 'service_role' then
raise exception 'billing columns can only be changed by the server';
end if;
return new;
end $$;
create trigger protect_billing before update on public.profiles
for each row execute function public.protect_billing_columns();
Fix 3 (cleanest): move billing to its own table
Keep subscriptions or credits in a separate table with a select policy for the owner and no insert, update or delete policies. Only your payment webhook writes to it, from an Edge Function using the service_role key (which stays a server secret).
Then let only the webhook set it
Your Stripe or Lemon Squeezy webhook should verify the signature, then set the plan with the service_role key on the server. Never set the plan from the "thank you" page in the browser: anyone can open that page.
A prompt that works in Lovable, Bolt or Cursor: "Users must not be able to change is_pro, plan or credits. Only the payment webhook, in an Edge Function that verifies the signature and uses the service role key, may set them. Remove all frontend code that writes these columns, revoke UPDATE on profiles from authenticated and grant it back only for the columns users may edit. Do not turn off RLS."
What the free scan checks
The LeakPatrol scan reads the JavaScript your live site loads and flags code that writes paid columns (is_pro, premium, plan, credits and similar) straight from the browser. It can't see your policies, so after fixing the code, run the console test above too.
Check your app in a minute
Free, read-only, no sign-up. Every problem comes with a plain-English fix and a prompt for your AI tool.
Get the 10-point checklist
Every check we run on AI-built apps, with how to do each one yourself. Straight to your inbox.
Plus an email when automatic re-checks launch, and the odd tip. Unsubscribe any time.
Questions
Can users change their own is_pro column in Supabase?
Yes, if the profiles table has an update policy like auth.uid() = id and is_pro is on that table. RLS limits which rows a user can update, not which columns.
How do I stop users updating a column in Supabase?
Revoke UPDATE on the table from the authenticated role and grant it back only for the columns users may edit, for example grant update (full_name, avatar_url) on public.profiles to authenticated. Or use a trigger that rejects changes unless auth.role() is service_role.
Where should a subscription status be set?
Only in your payment webhook on the server, after verifying the webhook signature, using the service_role key. Never from the browser or the checkout success page.
Does removing the update from my frontend fix it?
No. Anyone can send the same request by hand with your public anon key and their own login. The database has to refuse it.
More guides
- Supabase RLS policy generator (free)
- Is this API key safe in my frontend? Free key checker
- Security scan MCP server for Claude Code, Cursor and other AI agents
- Supabase's October 30 change: what it fixes for your app's security, and what it doesn't
- Lovable security check: is your Lovable app safe?
- Bolt.new security check: is your Bolt app safe?
- Launch checklist for apps built with Lovable, Bolt and Cursor
- Supabase security check: is your anon key safe? (RLS checker)
- How to check if your API key is exposed on your website
- Are VITE_ env variables secret? How to hide API keys