Supabase RLS for SaaS Billing: Keep Stripe Subscription Status Server-Side
When adding subscriptions to a Supabase app, the browser should never decide that a user is paid. Keep billing state on the server and use Row Level Security (RLS) to limit what each signed-in user can read. A common so
When adding subscriptions to a Supabase app, the browser should never decide that a user is paid. Keep billing state on the server and use Row Level Security (RLS) to limit what each signed-in user can read.
A common source of confusion is assuming that an βupdate your own profileβ policy makes every field on that row safe to edit. RLS policies control which rows a query can affect; PostgreSQL grants control which actions a role can perform. Billing fields such as subscription_status should be written only by trusted server-side code.
The risky pattern
Imagine a profiles table with an owner-scoped update policy and a broad table grant:
create policy "users update own profile"
on public.profiles for update
using (auth.uid() = id)
with check (auth.uid() = id);
grant select, insert, update, delete
on public.profiles to anon, authenticated;
That policy answers βwhich row?β It does not make subscription_status an approved column. A signed-in user who can update their own row could try to change that field from the client.
Keep billing writes server-side
If the browser only needs to read a userβs profile, a narrower permission setup is easier to reason about. Keep an owner-scoped SELECT policy in place, remove direct table writes from normal client roles, and let trusted server code process billing updates:
revoke all on public.profiles from anon, authenticated;
grant select on public.profiles to authenticated;
grant all on public.profiles to service_role;
This SQL changes table privileges; it does not create the SELECT policy. Keep a policy such as auth.uid() = id for profile reads. If users need editable fields, grant only those specific columns and keep subscription fields outside that allowlist. Adapt the grants to your schema and any signup flow that inserts profiles.
The Supabase service role key must stay on the server and must never be included in a frontend bundle. In a Stripe integration, update billing state after verifying the webhook signature against the untouched request body. A checkout success redirect is not proof of payment and should not unlock the paid UI by itself.
Try the demo
I built a small Vue 3 + Supabase + Stripe SaaS starter to practice this end-to-end flow. The hosted demo uses Stripe test mode; its sample $19/month price is a test price. The source kit is a separate, one-time Gumroad purchase.
I used AI assistance to edit the wording. The project and implementation are mine.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.