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
- 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.
- 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.
- 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.
- 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
- 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.
- 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.
- 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.
- 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