DEV Community

MrLong
MrLong

Posted on Edited on AI-assisted

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 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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)