SYS://FIELD-REPORT-01

The service_role Key Was in the Browser Bundle

The app worked. It looked finished. It was also one changed URL away from leaking every user’s data — and the owner would never have found out, because nothing was watching. Here’s the full teardown, worst finding first.

This is a representative audit — the exact pattern I run into over and over on apps built with Lovable, Bolt, v0, and Cursor. Names and paths are changed, but every finding below is real-shaped and drawn straight from the kind of report the launchworthy skill produces. Call the app taskflow: a Vite + React + Supabase task manager on Vercel, pre-launch, about to be shared with its first real users.

It got a scorecard. The scorecard was not kind.

0/5
domains passing
2
critical findings
8
high findings

Zero domains passing does not mean the app is bad. taskflow is genuinely nice. It means the app is finished-looking, which is a different and more dangerous thing, because finished-looking is exactly the state in which people hit deploy and tell their friends.

The finding that ends the app: RLS is off

The number one critical bug in AI-built apps is not exotic. It’s a Supabase table with row-level security turned off.

taskflow’s supabase/migrations/ folder had no enable row level security statement anywhere. Every table — tasks, profiles — was wide open. And here’s the part people miss: this isn’t a “someone determined enough could get in” problem. Supabase apps ship their anon key to the browser by design; anyone can read it from the network tab in about four seconds. With RLS off, that key reads and writes every user’s rows. Not their own. Everyone’s.

CRITICAL · CAUGHT

RLS disabled on tasks and profiles

The app works and looks finished. It is also one changed URL away from leaking every user’s tasks — and you would not find out, because nothing is being tracked.

The fix is not exotic either.

THE FIX · 15 MIN

Enable RLS and add ownership policies keyed on user_id. Do the enable and the policy in the same migration so you don’t lock yourself out between deploys. It’s the single highest-leverage change in the whole report.

The false alarm that wastes everyone’s time

Now the part that matters most for credibility, and the reason I trust an audit that gets it right.

Point a generic “review my code” session at taskflow and it will almost certainly flag this:

FALSE POSITIVE

⚠️ Your Supabase anon key is exposed in the client bundle!

This is wrong. The anon key is public by design — it’s meant to ship to the browser. Flagging it is the tell of a reviewer pattern-matching on the word “key” instead of understanding the threat model. Worse, it burns attention on a non-issue while the actual critical sits two lines away, unmentioned.

The distinction is the whole game:

  • anon key in the bundle → fine, by design. The anon key is only as powerful as your RLS policies let it be.
  • service_role key in the bundle → catastrophic. It bypasses RLS entirely.

Which brings us to taskflow’s second critical.

The second critical: service_role, shipped to every browser

src/lib/supabase.ts:4 created the client like this:

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_SERVICE_ROLE  // ← this ships to the browser
);

Any VITE_-prefixed variable is compiled into the client bundle and shipped to every visitor. And the service_role key bypasses RLS completely. So even after you fix finding one — even with perfect ownership policies — this single line hands full, unrestricted database access to anyone who opens DevTools.

There is no clever mitigation here. The key is compromised the moment the app was deployed.

THE FIX · IN ORDER

Rotate, then relocate

  1. Rotate the key in the Supabase dashboard, immediately. Assume the current one is burned.
  2. Use the anon key on the client.
  3. Keep service_role server-side only — never in a VITE_-prefixed variable, ever.

Verify it by hand Run npm run build, open the app, DevTools → Sources, search the bundle for service_role. Before the fix you get a hit; after, nothing. Screenshot both — that’s your evidence the fix landed.

The quieter FAIL: nobody would ever know

Auth is the loud failure. The second FAIL is quieter and, in its own way, worse: Operations & Recovery. If something goes wrong in production, taskflow’s owner finds out from an angry user, not from a system.

  • No error tracking. No Sentry, no global handler. Production throws into the void.
  • No error boundary. One render error in src/App.tsx white-screens the entire app for everyone.
  • No automated backups confirmed. On the Supabase free tier, backup retention is thin. A bad migration and there’s nothing to roll back to.
  • One environment. Dev and production share the same Supabase project, so a careless migration in dev can corrupt live data.

This is the category a code review structurally cannot catch, because it isn’t in the code. “Review my app” gets you a code review. It does not ask whether your alerts actually reach you, whether your backups have ever been restore-tested, or whether you can roll back a bad deploy. A third of what separates “works for me” from “real users depend on this” isn’t visible in the source at all — and the person who most needs those questions asked is exactly the person who doesn’t know to ask them.

The rest of the punch list

The two FAILs are the blockers. The rest is the week-two work, ranked so the scope stays honest:

FIX THIS WEEK

After the two criticals

  1. [HIGH] No rate limit on the endpoint that calls the OpenAI API. One script in a loop overnight = a four-figure bill.
  2. [HIGH] No input validation — req.body written straight to Supabase. Add a Zod schema.
  3. [MEDIUM] Missing security headers, no loading states, no empty states, an N+1 query on the dashboard, no uptime monitoring.
  4. [LOW] Fixed pixel widths on the sidebar that could be fluid.

Fourteen findings total. But note the shape: two of them can end the business, and twelve are homework. That ranking is the point. A flat checklist of fourteen items reads as fourteen equal chores. Worst-first with severities reads as “do these two things before you tweet the link, then breathe.”

Why this keeps happening

None of this means AI builders are bad, or that taskflow’s owner did anything foolish. The tools are extraordinary at getting you to looks finished. They are, by their nature, silent about the gap between that and safe to launch — because that gap is invisible on your own screen, where you’re logged in as yourself, nothing has broken yet, and there’s exactly one user: you.

That gap is the whole reason launchworthy exists. It plays bouncer at the door of production: it knows the difference between the anon key (fine) and RLS-off (critical), it refuses to mark anything safe it can’t actually verify, and it hands you the fix, not just the finding.

Two hours of work moved taskflow from 0/5, do-not-launch to safe to ship. The hard part was never the fixing. It was knowing which two things out of fourteen would’ve been the ones that breached you — and that the scariest-sounding one, the anon key everyone flags, wasn’t one of them.