Your MVP demos great. It also leaks your whole database.
Vibe-coded apps ship with the service_role key in the browser, no row-level security, and the OpenAI key in the bundle. Anyone with devtools reads every customer's data. I move trust from the browser to Postgres + the server — then deploy it safely.
real vulnerabilities fixed in the reference rescue
RLS
authorization enforced in Postgres, not React
1–3d
typical turnaround, deployed & keys rotated
The reference rescue — “FeedbackHQ”
8 holes every no-code export ships with
A representative AI feedback-triage SaaS, exactly as it comes out of Lovable/Bolt. Each row is a real issue and the exact fix. Left is what shipped; right is what I replace it with.
01CRITICAL
service_role key hardcoded in the browser
● Before
The client is built with the service_role key “so RLS doesn’t get in the way.” It bypasses all security — anyone who opens devtools has full read/write on every table.
✓ After
Browser uses the anon key only. service_role is removed from the bundle, used solely in trusted server jobs, and the exposed key is rotated.
02CRITICAL
OpenAI key shipped to the client
● Before
VITE_OPENAI_API_KEY + dangerouslyAllowBrowser. The key is extractable from the JS bundle — strangers spend on your account.
✓ After
The LLM call moves to a server route /api/triage. The key is a server-only env var and never reaches the browser.
03CRITICAL
No Row-Level Security
● Before
Tables are world-readable with the anon key; tenant isolation is a .filter() in React. One request returns every workspace’s tickets.
✓ After
RLS enabled on every table with per-workspace policies via auth.uid() and a workspace_members table. Postgres refuses cross-tenant rows.
04HIGH
Auth is a client-side redirect
● Before
if (!user) navigate('/login') in a component. The data layer is wide open — skip the UI and the API answers anyway.
✓ After
Auth enforced in middleware + server components; getUser() re-validates the JWT before anything protected renders.
05HIGH
Admin rights from localStorage
● Before
isAdmin = localStorage.role === 'admin'. Any user types one line in the console and grants themselves admin.
✓ After
Roles live in workspace_members.role and are enforced inside RLS (is_admin()), never trusted from the client.
06HIGH
Stored XSS in ticket bodies
● Before
User content rendered with dangerouslySetInnerHTML — a crafted ticket runs script in every teammate’s browser.
✓ After
Rendered as plain text; React escapes it. User input is never treated as markup.
07HIGH
Prompt injection & no validation
● Before
Raw user text dropped straight into the prompt, no length or type checks — trivially hijacks the model.
✓ After
Input validated with zod; user text passed as delimited data with an “ignore instructions” guard; model output schema-checked.
08MEDIUM
No rate limit + billing secrets exposed
● Before
The AI endpoint has no limit — a loop is a cost bomb. stripe_customer_id sits in a public-readable table.
✓ After
Per-user rate limit / spend cap on the endpoint; billing moved to a table with no client policy (server-only access).
The deliverable
A hardening checklist, run top to bottom
Every rescue ships with this checklist completed against your export — plus a second-account test that proves cross-tenant isolation actually holds.
Secrets & keys
service_role out of the frontend, rotated
No provider keys behind VITE_ / NEXT_PUBLIC_
Secrets in the host env store, not the repo
Database (RLS)
RLS on every table with user data
Tenant isolation via auth.uid() + membership
PII / billing columns clients can’t select
Auth
Enforced server-side, not a React redirect
Sessions validated with getUser()
Protected routes 401 the unauthenticated
AI / API calls
Runs server-side; key never in the browser
Authenticated and rate-limited
Injection-guarded; output schema-checked
Input / output
All inputs validated (length, type, enum)
No dangerouslySetInnerHTML on user content
Uploads type/size-checked; buckets scoped
Deploy & ops
Real migrations in the repo, not click-ops
Separate dev / prod projects & keys
Error tracking; no secrets/PII in logs
How it works
Send the repo. Get it back safe.
Async, no calls required — I work from your export and a written walkthrough.
STEP 01
Free audit
Share your Lovable/Bolt/v0 export or repo. I return a prioritized issue list — the same table you see above, against your code.
STEP 02
Harden
RLS migrations, server routes, secrets moved & rotated, validation, rate limits — wired to your Supabase project.
STEP 03
Prove & deploy
Second-account test proving cross-tenant isolation, dev/prod split, and a recorded walkthrough of every fix. Then it ships.
Is your key in the browser right now?
Open your deployed app, hit devtools → Sources, and search for “service_role”. If it’s there, you have a breach waiting to happen. Send it over — I’ll tell you exactly what’s exposed, free.