Sample report from a test app. NotesShop is a demo app we built to show what a VibePatchers report looks like. It is not a real customer, and all keys shown are fake.

Sample report: NotesShop (test app)

Service: Vibe-coded App Rescue ($149 flat, fix-and-verify) App: NotesShop (Vite + React front end, Supabase-style Postgres backend, Stripe checkout) Result: 3 issues found, 3 fixed. Verification suite: 2/14 passing before → 14/14 passing after.

Summary for the owner

#ProblemRisk if left aloneStatus
1Database security rules let anyone read and change everyone’s dataAny visitor could download every user’s notes and orders, plant data in other accounts, or mark orders “paid”✅ Fixed
2Secret admin keys were shipped inside the website codeAnyone could open the browser dev tools, copy the keys, and get full admin access to your database and charge/refund through your Stripe account✅ Fixed (you must rotate the keys — see below)
3Payment webhook accepted fake payment noticesAnyone could send a fake “payment succeeded” message and unlock paid features for free; real Stripe retries were recorded twice✅ Fixed

1. Database rows open to everyone (row-level security)

What was wrong: The notes table had a policy named “Enable all access” (using (true)), which turns the security rules off in practice. The orders table had no row-level security at all. The anonymous (logged-out) role had full table permissions. What we changed: Removed the permissive policy; turned on and enforced row-level security on notes, orders, and payment_events; removed all table access for logged-out visitors; signed-in users can read/write only their own notes and only read their own orders. Order status can now only be changed by the server (the payment webhook). Change is a single new migration: supabase/migrations/002_rescue_rls.sql. Evidence: checks A1–A7 (below). Before: a logged-out visitor saw “Alice private” and “Bob private”, inserted a note into Bob’s account, and marked Bob’s order paid. After: all blocked, and Alice can still use the app normally (A6).

2. Secret keys in the browser bundle

What was wrong: The Supabase service-role key (bypasses all security rules) and the Stripe secret key were in .env with the VITE_ prefix, which tells the build tool to paste them into the public JavaScript. We found both in the built file dist/assets/index-*.js. The .env file was also committed to git. What we changed: The browser now only uses the public anon key plus the signed-in user’s session. Secret keys were renamed to server-only variables (no VITE_ prefix). Checkout sessions are created by the server (/api/create-checkout-session) instead of calling Stripe from the browser. .env is removed from git and ignored; .env.example documents which keys are safe for the browser. Action required from you: because these keys were public, treat them as stolen. Rotate the Supabase JWT secret/service-role key and roll the Stripe secret key, then update the server environment. (Removing them from the code does not un-leak them; they also remain in old git history.) Evidence: checks B1–B3. Before: bundle contained a service_role key and sk_test_…. After: only the public anon key is present.

3. Stripe webhook accepted forged events

What was wrong: The handler parsed the JSON first and then re-encoded it to check Stripe’s signature. Re-encoding changes the bytes, so the check always failed — and the code had been “fixed” to log a warning and continue anyway. Result: any request claiming to be Stripe was trusted. It also had no protection against Stripe’s automatic retries. What we changed: The signature is now checked against the exact raw request body; invalid or missing signatures get HTTP 400 and nothing is recorded. Each Stripe event ID is stored once (unique constraint) and the order is only marked paid on first delivery, inside a database transaction. Database errors return 500 so Stripe retries. Evidence: checks C1–C4. Before: unsigned and wrongly-signed events returned 200 and marked order #2 paid; a retry was recorded twice. After: forged events get 400, order stays pending; the genuine signed event marks the order paid exactly once.

Verification results

Before (customer baseline, commit 4b5dd2b + checks c2d6a5e)

CheckWhat it provesResultEvidence
A1Anonymous visitor cannot read any user notes❌ FAILanon saw 2 note(s): Alice private, Bob private
A2Anonymous visitor cannot read orders❌ FAILanon saw 2 order(s)
A3Anonymous visitor cannot insert a note as another user❌ FAILHTTP 201
A4Anonymous visitor cannot mark an order paid❌ FAILrows updated: 1
A5Signed-in Alice sees only her own notes (not Bob’s)❌ FAILalice saw: Alice private, Bob private, pwned
A6Signed-in Alice can still create her own note (no regression)✅ PASSHTTP 201
A7Signed-in Alice cannot write a note into Bob’s account❌ FAILHTTP 201
B1No Supabase service-role key in built bundle❌ FAILJWTs in bundle: service_role
B2No Stripe secret/restricted key or webhook secret in built bundle❌ FAILfound: sk_test_FAKE_rescuelab_51NotARealKey000000000000
B3No secret env vars exposed via VITE_ prefix in .env / source❌ FAILfound: VITE_SUPABASE_SERVICE_ROLE_KEY, VITE_STRIPE_SECRET_KEY
C1Forged event with NO signature is rejected and order stays unpaid❌ FAILHTTP 200, order#2 status=paid
C2Event signed with the WRONG secret is rejected❌ FAILHTTP 200, order#2 status=paid
C3Genuine signed event is accepted and order is marked paid✅ PASSHTTP 200, order#1 status=paid
C4Stripe retry of the same event is not recorded twice❌ FAILHTTP 200, recorded 2x

After (branch rescue/fix-security, commit b3fe5f9)

CheckWhat it provesResultEvidence
A1Anonymous visitor cannot read any user notes✅ PASSanon saw 0 note(s)
A2Anonymous visitor cannot read orders✅ PASSanon saw 0 order(s)
A3Anonymous visitor cannot insert a note as another user✅ PASSHTTP 400
A4Anonymous visitor cannot mark an order paid✅ PASSrows updated: 0
A5Signed-in Alice sees only her own notes (not Bob’s)✅ PASSalice saw: Alice private
A6Signed-in Alice can still create her own note (no regression)✅ PASSHTTP 201
A7Signed-in Alice cannot write a note into Bob’s account✅ PASSHTTP 400
B1No Supabase service-role key in built bundle✅ PASSJWTs in bundle: anon
B2No Stripe secret/restricted key or webhook secret in built bundle✅ PASSnone found
B3No secret env vars exposed via VITE_ prefix in .env / source✅ PASSnone
C1Forged event with NO signature is rejected and order stays unpaid✅ PASSHTTP 400, order#2 status=pending
C2Event signed with the WRONG secret is rejected✅ PASSHTTP 400, order#2 status=pending
C3Genuine signed event is accepted and order is marked paid✅ PASSHTTP 200, order#1 status=paid
C4Stripe retry of the same event is not recorded twice✅ PASSHTTP 200, recorded 1x

Note on C3 “before”: the genuine event was recorded, but only because signature checking was effectively off — the same code also accepted forged events (C1, C2).

How to reproduce

cd app && npm install
node checks/run-all.mjs        # resets the local DB, builds the bundle, runs all 14 checks; exit 0 = all pass
git diff main rescue/fix-security   # every change we made

Not covered by this engagement

Authentication flow review, other tables/functions/storage buckets beyond those listed, rate limiting, and anything in your live Supabase/Stripe dashboards that isn’t represented in the code (e.g. policies created by clicking in the dashboard). We recommend a quick dashboard check after deploy.

Want this report for your app?

See pricing