Lovable · Bolt.new · v0 · Replit → production

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.

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

Next.js · Supabase (RLS / @supabase/ssr) · zod · server-side LLM proxy · Vercel