heydevTalk to founders

Startup TechAuthentication

Vibe-Coded App Audit: What It Checks, What It Costs, and a Sample Report

Akshit Ahuja

Akshit Ahuja

Co-founder, Chief Speed Officer

Published
A printed audit report on a dark desk, with findings in its table marked by glowing red tags and a fountain pen resting across the page

You built the app with Lovable, Bolt, Replit or Cursor. It works, people are signing up, and someone has asked "is it secure?" That might be a customer, an investor, or the developer you are about to hire. You don't know the answer, and the AI that wrote the code will cheerfully tell you it is.

An audit gets you that answer. This post covers what a vibe-coded app audit checks, what the market charges, and a full sample report. The report is not from a client. It comes from a small demo app we built in the style AI builders produce and then audited the same way we audit real ones. Every finding below was found and tested, and the outputs are copied from the terminal.

What an audit is, and what it is not

A vibe-coded app audit is a senior engineer reading your codebase and testing your database and APIs the way an attacker would, then handing you a ranked list of what is broken, how bad it is, and exactly how to fix each item.

It is not a penetration test of your live servers, a compliance certificate, or a promise that nothing can ever go wrong. It also is not an automated scan, though it starts with one. The difference matters. Symbiotic Security scanned 1,072 vibe-coded apps in June 2026 and found flaws in 98% of them, with 172 that let anyone delete database records without logging in. Scanners are good at finding that kind of thing. They are bad at finding a security rule that is switched on but wrong, and that is where the expensive bugs live.

What it checks

Seven areas, in the order we work through them:

AreaWhat we look forFound by
SecretsAPI keys in the browser bundle, the Supabase service key anywhere outside the server, keys in git historyScan of the built bundle and repo history
Database accessTables with Row Level Security off, policies that allow too much, policies with no `with check`, default grants to the public rolesRunning queries as a logged-out visitor and as two different users
Auth and rolesAdmin checks that only exist in the React code, users who can edit their own role, sessions that never expireReading the auth flow end to end, then trying to break it
PaymentsPrices the browser sends, webhooks that do not verify Stripe's signature, double-credit on retried eventsReading every function that touches money
Third-party APIsOpenAI or other paid keys called from the browser, no rate limits on anything that costs money per callBundle scan plus code reading
PerformanceMissing indexes on the columns every page filters by, queries that load whole tables`EXPLAIN ANALYZE` on the queries the app actually runs
DependenciesPackages with known vulnerabilities, and whether those packages ship to production or only run on your laptop`npm audit`, then judging each one

For more detail on the security side, see the twelve checks in AI-generated code security: the numbers and a 12-point check.

What it costs

Published prices from providers who show them, checked on October 3, 2026:

ProviderPriceTurnaroundNotes
[Valletta Software](https://vallettasoftware.com/blog/post/vibe-coding-audit)From $199 / $799 / $1,4993 / 5 / 7 business daysTiers by app size: under 5,000 lines, 5,000 to 25,000 lines, investor-ready
[Fortivibe](https://fortivibe.com/)$499, or $999 expedited3 to 5 days, or 1 business dayOne repository, human-reviewed findings
[VibeAudits](https://vibeaudits.com/code-audit)Not published on the page1 to 2 business days for first findings
**HeyDev****$999****5 business days**Apps up to about 25,000 lines. Includes database tests as two users, a 45-minute walkthrough call, and a re-check of your critical fixes

The price mostly reflects how much of the work is a person versus a tool. At $199 you are mostly buying a tool run with a human summary. Somewhere around $500 to $1,000, someone actually reads the code. Above that, you are paying for a document written for an investor or acquirer to read.

Ours is priced in that middle band and goes as deep as the sample below: every table tested as a logged-out visitor and as two different users, through the same requests your app's client library sends, by engineers from Microsoft and IIT Kanpur. That depth is what found Findings 5 and 7: the security rules were switched on, so a scan reports them as fine.

The sample report

The app

"Invoicely" is a small invoicing tool for freelancers: Vite + React frontend, Supabase for auth and data, two Supabase Edge Functions for Stripe checkout and the Stripe webhook, and an "AI draft reminder" button that calls OpenAI. That is what Lovable, Bolt and Replit typically produce. It has three tables (profiles, clients, invoices) and about 150 lines of app code. For testing we loaded 50,000 invoices across 502 accounts.

We wrote the flaws in on purpose. Each one is a pattern we see repeatedly in AI-built apps, and the Symbiotic scan found the same patterns at scale.

Summary

#FindingSeverityFix effort
1Supabase service role key shipped in the browser bundleCritical1 to 2 hours
2`invoices` table has Row Level Security off: anyone can read and edit all 50,000 invoicesCritical30 minutes
3Stripe webhook does not verify signatures: anyone can mark any invoice paidCritical1 hour
4OpenAI API key shipped in the browser bundleHigh2 hours
5Any user can make themselves an adminHigh30 minutes
6Checkout charges whatever amount the browser sendsHigh1 hour
7Any user can create clients inside another user's accountMedium15 minutes
8Admin page is protected only in ReactMedium1 hour, after #1
9No index on `invoices.user_id`Low5 minutes
104 vulnerable npm packages, none critical in productionLow1 hour

Three criticals, about nine hours of fixes in total. That ratio is typical: the bugs are severe, but each fix is small once you know where it is.

Finding 1: the service role key is in the browser (Critical)

The app creates a second Supabase client "so the dashboard can see all invoices without RLS errors", using VITE_SUPABASE_SERVICE_ROLE_KEY. Vite copies every VITE_-prefixed variable into the JavaScript it ships to visitors. We built the app for production and searched the output:

bundle
$ grep -oE 'eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+' dist/assets/*.js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9...
   payload: {"role":"service_role","demo":true}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImR...
   payload: {"role":"anon","demo":true}

The anon key is meant to be public. The service role key is not. Supabase's docs say it bypasses Row Level Security entirely. Anyone who opens the browser developer tools has full read, write and delete access to every table, including the users list.

Fix: rotate the key in Supabase now; until you do, assume it has already been copied. Delete the admin client from the frontend and move admin reads into an Edge Function that checks the caller's role on the server.

Finding 2: anyone can read and edit every invoice (Critical)

The migration has a comment where the security switch should be: -- TODO: enable RLS after launch, it was blocking the dashboard. We listed every table's status:

RLS
  table   | rls_on | policies
----------+--------+----------
 clients  | t      |        2
 invoices | f      |        0
 profiles | t      |        2

Then we connected the way a logged-out visitor does, with only the public anon key:

as
 rows_visible | accounts_exposed | total_usd
--------------+------------------+-----------
        50000 |              502 | 125256217

-- update invoices set status = 'paid'
 rows_updated
--------------
        50000

Every customer's invoices are readable and editable by anyone. (We ran the update inside a transaction and rolled it back.)

Fix: alter table invoices enable row level security; plus a policy limiting each user to their own rows, for both reading (using) and writing (with check). Then fix the dashboard query that RLS was "blocking", which is usually the real bug. Supabase is changing how new tables get exposed to the API from October 30; we covered what that changes in Supabase stops auto-exposing new tables.

Finding 3: anyone can mark an invoice paid (Critical)

The Stripe webhook reads the request body and marks the invoice in metadata.invoice_id as paid. It never checks that the request came from Stripe:

supabase/functions/stripe-webhook/index.ts
Deno.serve(async (req) => {
  const event = await req.json()
  if (event.type === 'checkout.session.completed') {
    const invoiceId = event.data.object.metadata.invoice_id
    await supabase.from('invoices').update({ status: 'paid' }).eq('id', invoiceId)
  }
  return new Response(JSON.stringify({ received: true }), { status: 200 })
})

A POST with a made-up checkout.session.completed event marks any invoice paid without any money moving. We found this by reading the code, not by sending forged events to a live Stripe account.

Fix: verify the Stripe-Signature header with stripe.webhooks.constructEventAsync and your webhook signing secret before parsing anything (Stripe's docs). Read the invoice ID from the verified event, and ignore events you have already processed, because Stripe retries.

Finding 5: any user can make themselves an admin (High)

The profiles table has a role column, and the update policy lets users edit their own row. It does not stop them editing role:

as
update profiles set role = 'admin' where id = auth.uid();
UPDATE 1
                  id                  | role
--------------------------------------+-------
 11111111-1111-1111-1111-111111111111 | admin

Right now that only unlocks the admin page. The first time someone writes a server function that trusts role, it becomes a full takeover. AI tools write that policy because "users can update their own profile" sounds correct, and for the name column it is.

Fix: move role to a table users cannot write to, or limit which columns users can update. Revoking only the role column does nothing, because Supabase grants update on the whole table, and a table-wide grant overrides a column revoke. Revoke the table grant and give back only the safe columns: revoke update on profiles from authenticated; grant update (full_name) on profiles to authenticated;.

Finding 7: users can write into other accounts (Medium)

The insert policy on clients is with check (true). Our first test used insert ... returning and Postgres blocked it, because returning the row also requires permission to read it. The Supabase JavaScript client does not ask for rows back unless you call .select(), so we ran it again the way the app sends it:

as
INSERT 0 1
      owner      |       name
-----------------+-------------------
 bob@example.com | Globex
 bob@example.com | Injected by Alice

That is why we test the way the client library actually talks to the database, not the way a textbook query would.

Fix: with check (auth.uid() = user_id).

Finding 9: the dashboard reads the whole table (Low)

The dashboard filters invoices by user_id, which has no index. Postgres reads all 50,000 rows and throws away 49,901 to return Alice's 99:

PlanPages readTime
BeforeSequential scan1,429about 3 ms
After `create index on invoices(user_id)`Index scan4about 0.014 ms

At 50,000 rows, 3 ms is fine, which is why this is rated Low. The cost grows with the size of the whole table, not with any one user's data, so at a few million rows it is the reason the dashboard feels slow. It takes five minutes to fix now and a stressful afternoon later.

The rest, briefly

  • 4. OpenAI key in the bundle (High). Same scan as Finding 1 found an sk-proj- key. Anyone can spend your OpenAI credit. Move the call to an Edge Function and add a per-user rate limit.
  • 6. Checkout trusts the browser's amount (High). create-checkout charges amount_cents from the request body, so a $5,000 invoice can be paid for $0.50. Look up the amount from the invoice on the server. The frontend also sends no auth header, which means either JWT verification is switched off on that function, so anyone can call it, or the Pay button does not work in production. Either is worth knowing.
  • 8. Admin page guarded only in React (Medium). profile?.role === 'admin' hides the page but protects nothing. The fix comes with Finding 1: the server decides who is an admin.
  • 10. Dependencies (Low). npm audit reported 4 packages (1 high, 3 moderate). The high one is in Vite's development server, which never ships to users. The React Router open-redirect advisory does ship and is worth the upgrade. A good audit tells you which ones matter; a raw npm audit makes them all look equally urgent.

How to tell a good audit from a bad one

Before you pay anyone, including us, ask for a sample report and check it for four things:

  1. Evidence. Each finding shows the query, request or line of code that proves it. "Possible SQL injection risk" without proof is a scanner talking.
  2. Tests as two users. Most of the serious findings above only appear when you act as one user and try to reach another user's data.
  3. Severity you can act on. Ten "High" findings is not a priority list. You want three things to fix today and the rest ranked behind them.
  4. A fix and a time estimate for each. You or your AI tool should be able to act on the report without hiring the auditor.

What we did not test

The database tests ran against local Postgres 16 with Supabase's anon and authenticated roles, its auth.uid() function and its default table grants recreated, not against a hosted Supabase project through its REST API. Row Level Security evaluates the same way in both, but we have not confirmed every result through the hosted API. Findings 3, 4, 6 and 8 come from reading the code; we did not exploit them against live Stripe or OpenAI accounts. The demo app is small and its flaws were planted, so it shows what an audit finds, not how often real apps have each flaw. For frequency, use the Symbiotic numbers above.

Getting one

The HeyDev audit is $999, delivered in 5 business days, for apps up to about 25,000 lines built with Lovable, Bolt, Replit, Cursor, Base44 or by hand. You get a report in the format above, a 45-minute walkthrough call, and a free re-check of your critical fixes. If you would rather we did the fixes, we quote them after the report.

Book a 30-minute call or email mahima@heydev.us with your repo and what the app does.

FAQ

Frequently asked questions

Is an automated scanner enough, or do I need a person?
A scanner is a good first pass and catches exposed keys, known-vulnerable packages and tables with Row Level Security turned off. It does not catch a policy that is switched on but wrong, like an update policy that lets a user change their own role, or a checkout that trusts the price the browser sends. In our sample audit, a scanner would have caught 4 of the 10 findings.
Will an audit fix the problems, or just list them?
It lists them, ranked, with the exact fix for each and a time estimate. Most founders fix the criticals themselves or with their AI tool once they know exactly what to change. If you want us to do the fixes, we quote them separately after the report, so you never pay for fixes you could have done yourself.
Do you need access to my production database?
No. We need read access to the code repository and, for Supabase apps, either a read-only database connection to a staging copy or the migration files. Tests that write data run inside transactions that are rolled back, or against a copy.
How long does a vibe-coded app audit take?
Published turnarounds run from 1 to 7 business days depending on app size and provider. Ours is 5 business days for apps up to about 25,000 lines of code. The demo app in this post took under a day because it is small.
When should I get an audit?
Before the first paying customer, before you send the codebase to an investor or acquirer, and before you hire your first developer. Each of those is a moment when someone else's money or data starts depending on the code.

Sources

  1. 01Symbiotic Security: We scanned 1,072 vibe-coded apps, 98% had security flaws (June 2026)
  2. 02Valletta Software: Vibe coding audit pricing
  3. 03Fortivibe: Launch audit pricing
  4. 04VibeAudits: Code audit for startups
  5. 05Supabase docs: Row Level Security
  6. 06Supabase docs: API keys
  7. 07Stripe docs: Verify webhook signatures

#vibe coding #code audit #security audit #Supabase #Row Level Security #Lovable #Stripe webhooks #founders

Written by

Akshit Ahuja

Akshit Ahuja

Co-founder, Chief Speed Officer

Ex-Spinny, a $1B+ startup / Backend and AI systems

Backend systems specialist who thrives on building reliable, scalable infrastructure. Akshit handles everything from API design to third-party integrations, ensuring every product HeyDev ships is production-ready.

Keep reading

Related articles