PutThrough Product engineering studio
Book a build call

Demo rescue

A subscription dashboard with an open database

The most common shape of the problem: an app that works perfectly for one logged-in user and hands every row of every table to anyone who opens the network tab.

Built with Lovable on Supabase and Stripe

A demonstration build, not a client engagement. We built this app, broke it, and repaired it to show the method. No client is named or implied, and the figures below are measurements from this build alone.

Stack
  • Lovable
  • React
  • Supabase
  • Postgres
  • Stripe
  • Vercel
Duration
[n] days

01Before

What was broken

  1. S1

    Row-level security was never enabled

    Every table was reachable with the public browser key, which ships in the JavaScript bundle by design. Any visitor could read all user records, all subscription records and all customer data, and could write to them. The application itself showed no sign of this — the UI filtered by user, so the product looked correct.

  2. S2

    The service role key was in the client bundle

    A key intended for server-side use only had been used in client code to work around the access rules that were never configured. It bypasses row-level security entirely, so enabling RLS alone would not have closed the hole.

  3. S3

    Stripe webhooks were unverified and not idempotent

    The webhook endpoint accepted any POST without checking the signature, so subscription status could be set by anyone who found the URL. It also had no idempotency handling, so Stripe’s ordinary retry behavior created duplicate subscription rows.

  4. S4

    Authorization was enforced in the interface

    Admin-only actions were hidden by a conditional in the React component. The underlying endpoints had no check at all, so the actions could be called directly.

02After

What we changed

  1. C1

    Access policies written per table and tested

    Row-level security enabled on [n] tables, with explicit read and write policies per role. Each policy has a test that asserts a user of one tenant cannot reach another’s rows — so a future schema change that breaks isolation fails the build rather than shipping.

  2. C2

    Service key removed, rotated, and moved server-side

    Every call that required elevated access moved behind server-side endpoints. The exposed key was rotated, and a build-time check now scans the client bundle for key patterns and fails the build if one reappears.

  3. C3

    Webhook signature verification and idempotency

    Signature verification against the Stripe signing secret, an idempotency key stored per event, and subscription state reconciled against Stripe as the source of truth rather than written blind. Failure, refund and cancellation paths implemented and tested.

  4. C4

    Authorization moved to the server

    Every privileged action now checks the caller’s role server-side. The interface still hides what you cannot do, but hiding is no longer what stops you.

03Measured

What it measured

Measure Before After
Tables with access policies 0 [n]
Secrets reachable in the client bundle [n] 0
Duplicate subscription records per 100 webhook retries [n] 0
Endpoints enforcing authorization server-side [n] of [n] [n] of [n]
Time to rescue — [n] days

Figures are measurements from this demonstration build. All demo rescues

Your app probably fails in one of these ways.

The audit tells you which, with evidence and a fix estimate, in two business days. If it finds nothing material you do not pay for it.

Prefer to write? Send your requirements

What do you need?

What it should do, who will use it, the must-haves, and any links. A few sentences is plenty.

Budget, if you have one in mind
When would you like to start?