
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:
| Area | What we look for | Found by |
|---|---|---|
| Secrets | API keys in the browser bundle, the Supabase service key anywhere outside the server, keys in git history | Scan of the built bundle and repo history |
| Database access | Tables with Row Level Security off, policies that allow too much, policies with no `with check`, default grants to the public roles | Running queries as a logged-out visitor and as two different users |
| Auth and roles | Admin checks that only exist in the React code, users who can edit their own role, sessions that never expire | Reading the auth flow end to end, then trying to break it |
| Payments | Prices the browser sends, webhooks that do not verify Stripe's signature, double-credit on retried events | Reading every function that touches money |
| Third-party APIs | OpenAI or other paid keys called from the browser, no rate limits on anything that costs money per call | Bundle scan plus code reading |
| Performance | Missing indexes on the columns every page filters by, queries that load whole tables | `EXPLAIN ANALYZE` on the queries the app actually runs |
| Dependencies | Packages 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:
| Provider | Price | Turnaround | Notes |
|---|---|---|---|
| [Valletta Software](https://vallettasoftware.com/blog/post/vibe-coding-audit) | From $199 / $799 / $1,499 | 3 / 5 / 7 business days | Tiers by app size: under 5,000 lines, 5,000 to 25,000 lines, investor-ready |
| [Fortivibe](https://fortivibe.com/) | $499, or $999 expedited | 3 to 5 days, or 1 business day | One repository, human-reviewed findings |
| [VibeAudits](https://vibeaudits.com/code-audit) | Not published on the page | 1 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
| # | Finding | Severity | Fix effort |
|---|---|---|---|
| 1 | Supabase service role key shipped in the browser bundle | Critical | 1 to 2 hours |
| 2 | `invoices` table has Row Level Security off: anyone can read and edit all 50,000 invoices | Critical | 30 minutes |
| 3 | Stripe webhook does not verify signatures: anyone can mark any invoice paid | Critical | 1 hour |
| 4 | OpenAI API key shipped in the browser bundle | High | 2 hours |
| 5 | Any user can make themselves an admin | High | 30 minutes |
| 6 | Checkout charges whatever amount the browser sends | High | 1 hour |
| 7 | Any user can create clients inside another user's account | Medium | 15 minutes |
| 8 | Admin page is protected only in React | Medium | 1 hour, after #1 |
| 9 | No index on `invoices.user_id` | Low | 5 minutes |
| 10 | 4 vulnerable npm packages, none critical in production | Low | 1 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:
$ 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:
table | rls_on | policies
----------+--------+----------
clients | t | 2
invoices | f | 0
profiles | t | 2Then we connected the way a logged-out visitor does, with only the public anon key:
rows_visible | accounts_exposed | total_usd
--------------+------------------+-----------
50000 | 502 | 125256217
-- update invoices set status = 'paid'
rows_updated
--------------
50000Every 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:
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:
update profiles set role = 'admin' where id = auth.uid();
UPDATE 1
id | role
--------------------------------------+-------
11111111-1111-1111-1111-111111111111 | adminRight 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:
INSERT 0 1
owner | name
-----------------+-------------------
bob@example.com | Globex
bob@example.com | Injected by AliceThat 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:
| Plan | Pages read | Time | |
|---|---|---|---|
| Before | Sequential scan | 1,429 | about 3 ms |
| After `create index on invoices(user_id)` | Index scan | 4 | about 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-checkoutchargesamount_centsfrom 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 auditreported 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 rawnpm auditmakes 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:
- Evidence. Each finding shows the query, request or line of code that proves it. "Possible SQL injection risk" without proof is a scanner talking.
- 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.
- 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.
- 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
- 01Symbiotic Security: We scanned 1,072 vibe-coded apps, 98% had security flaws (June 2026)
- 02Valletta Software: Vibe coding audit pricing
- 03Fortivibe: Launch audit pricing
- 04VibeAudits: Code audit for startups
- 05Supabase docs: Row Level Security
- 06Supabase docs: API keys
- 07Stripe docs: Verify webhook signatures
#vibe coding #code audit #security audit #Supabase #Row Level Security #Lovable #Stripe webhooks #founders
Written by

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.


