Supabase is the default backend of the vibe-coding era. Lovable, Bolt, Cursor, and v0 all reach for it, and the result is a generation of production apps whose database was configured by a language model that has never been paged at 3am. Over the past year we have audited 25 of those databases as part of rescue and completion engagements. Before the aggregate, the anatomy of the one that convinced us this deserved a study of its own.
Anatomy: the day the anon key could write to every table
The founder ran a B2B quoting and invoicing tool built in Lovable over six weeks: 31 tables, around 860 signed-up users, real invoices with real client names moving through it daily. The trigger was mundane. A prospective enterprise customer sent over a security questionnaire, and question four asked how row-level access control was enforced. The founder asked us to write the answer.
Day 1, discovery.The first check in any Supabase audit takes two minutes and needs no credentials you don't already have. The anon key ships in the client bundle by design; it is meant to be public, because Row Level Security is supposed to be the wall behind it. We lifted the key from the browser's network tab, pointed a 12-line script at the project's PostgREST endpoint, and walked the schema. Of the 31 tables, 9 had RLS switched off entirely, including invoices, quotes, and a table the generator had helpfully named api_credentials. Another 14 had RLS enabled with policies that amounted to using (true)for any authenticated user. The public anon key, the one any visitor's browser holds, could insert, update, and delete rows in every table that mattered. The security questionnaire answer, at that moment, was: there is no row-level access control, there is a polite request.
Days 2 to 3, the RLS rewrite. We wrote per-verb policy pairs for 27 of the 31 tables; the remaining 4 were genuinely public reference data (currency lists, plan catalogues) and were locked to read-only instead. Each policy got a plain-SQL test that runs in CI: sign in as role A, attempt the forbidden read or write, assert the row count is zero. Policies without tests rot, and generated policies rot fastest because nobody ever read them in the first place.
Day 4, keys and indexes. We rotated the JWT secret, which reissues both the anon and service-role keys, and shipped the new anon key in the same deploy as the hardened policies. Then the performance pass: 19 foreign-key columns had no covering index. The quotes list view, the screen every user lands on, was running a sequential scan over the largest table in the database. Two composite indexes took its p95 from 1,340ms to 74ms. Nobody had complained yet, because at 860 users nobody complains. At 8,600 they stop logging in instead.
Day 5, the boring layer. Upgrade to the Pro plan for daily backups, then the step almost everyone skips: an actual restore, into a scratch project, timed and documented. Environment separation (the founder had been prompting Lovable against the production database all along, one confident migration away from disaster). Auth hardening: email confirmations switched on, the site URL corrected from localhost:3000, rate limits on the auth endpoints. Invoice for the whole engagement: $4,500.
Did anyone exploit the hole during the six weeks it was open? The honest answer is that we cannot prove it either way: log retention on the plan the app was running only reached back seven days, and those seven days were clean. That ambiguity, not any single vulnerability, is the real cost of shipping a generated backend unaudited. That rescue is one of 25 in the dataset below.
TL;DR
- Only 5 of 25 databases had correct RLS on every table holding user data. 9 had RLS switched off outright on at least one user-data table, 11 had policies that looked right and were not, and 14 were writable with the public anon key.
- Index debt is universal but cheap; billing exposure is invisible until the invoice. 21 of 25 databases were missing indexes on foreign-key columns (median 7 per database), and the median Billing Bomb Index was 12.1: a 10x traffic month multiplies the bill roughly twelvefold.
- None of this is an argument against Supabase or against AI generators. It is an argument for a hardening pass before launch. The median rescue in this sample took five engineering days, against a median six weeks the founder spent building the prototype.
The backdrop: most new databases are now written by machines
This study lands at a specific moment. Supabase raised $500M at a $10.5B valuation in June 2026, with database launches on the platform up 600% year on year and, by the company's own account, over 60% of new databases now created by AI tools. That is not a niche behaviour to warn hobbyists about; it is the majority path onto the platform. The security model those tools configure, or fail to configure, is now the default security posture of a meaningful slice of the internet's new applications.
It also changes who is reading a post like this one. The people shipping these databases are founders, operators, and domain experts, not database administrators, and most of them have never heard the phrase row level security spoken aloud. That is not a criticism; it is the customer profile the entire category was built for. But it means the safety checks that used to be implicit in hiring a backend engineer now have to happen explicitly, as a deliberate step, or they do not happen at all. This study is our attempt to show, with numbers, exactly which checks those are and what happens when they are skipped.
We have covered the front-end half of this story already: our 31-codebase audit of Lovable / Bolt / v0 / Cursor prototypes catalogued what survives production in the application layer, and our cost and timeline study across 20 engagements put numbers on the graduation path. This post is the backend companion: the same rescue pipeline, but looking only at the database underneath.
Methodology
25 databases: 11 generated primarily with Lovable, 6 with Bolt, 5 with Cursor, 3 with v0, all audited between mid 2025 and mid 2026 as part of rescue or completion engagements. Project owners gave consent for anonymised inclusion. Every identifying detail (names, table names beyond generic examples, traffic figures) is either anonymised or rounded.
The audit scope was standardised across all 25: RLS coverage and correctness tested per table and per role with a scripted probe (not by reading the policies, by attempting the forbidden operations); key exposure checked in client bundles, repos, and Edge Function responses; auth configuration reviewed against the Supabase dashboard; index debt measured by cross-referencing foreign-key and hot lookup columns against pg_stat_statementssnapshots; backup and restore posture documented; and billing exposure modelled by scaling each project's current usage to 10x traffic using GA4 baselines and the public Supabase price sheet. Findings are classified by their worst instance: a database with one open table and twenty locked ones counts as open, because attackers do not average. Excluded: databases we designed ourselves from scratch, and any project that had already been through a professional hardening pass, since both would flatter the numbers. All figures are counts out of 25 unless stated otherwise; percentages are rounded to whole numbers.
Finding 1: RLS was fully correct on 5 databases out of 25
Chart 1: RLS status across 25 AI-generated databases
Each database classified by its worst table holding user or client data.
| RLS status at intake | Databases (n) | Share of 25 |
|---|---|---|
| RLS disabled on at least one user-data table | 9 | 36% |
| RLS enabled, but at least one policy materially wrong | 11 | 44% |
| RLS correct on every user-data table | 5 | 20% |
Sources: 25 anonymised audit engagements through Appycodes (2025-2026); Supabase dashboard exports; scripted PostgREST role probes; pg_stat_statements snapshots.
The disabled cases are the ones that make headlines, but the enabled-and-wrong cases are the ones that worry us more, because the dashboard shows a reassuring green shield on every table. The dominant defect is a single broad policy, usually named something cheerful like "Enable all access for authenticated users", that grants every signed-in user every verb on every row. The second most common is a correct select policy with no with checkclause on writes, so users can read only their own rows but insert rows into anyone's account.
Storage deserves a mention here too, because bucket policies are RLS's forgotten sibling. Supabase Storage enforces access with the same policy machinery, and the generators treat it the same way: of the databases in our sample that stored user files, most either marked the bucket public outright or attached a policy allowing any authenticated user to read every object. If your product stores contracts, invoices, or identity documents, the bucket policy is exactly as load-bearing as the table policy, and it gets a fraction of the attention.
-- What the generator shipped. Looks secure in the dashboard. Is not.
alter table invoices enable row level security;
create policy "Enable all access for authenticated users"
on invoices for all
to authenticated
using (true);
-- What production needs: one policy per verb, scoped to the owner.
create policy "invoices_select_own"
on invoices for select
to authenticated
using (auth.uid() = owner_id);
create policy "invoices_insert_own"
on invoices for insert
to authenticated
with check (auth.uid() = owner_id);
create policy "invoices_update_own"
on invoices for update
to authenticated
using (auth.uid() = owner_id)
with check (auth.uid() = owner_id);What correct looks like at slightly larger scale: the CA Compliance Calendar we built for an Indian CA firm runs three roles (Admin, Team, Client) over GST, ROC, and Income Tax deadlines, with the access model enforced entirely in RLS: clients see only their own submissions, team members reach clients through an assignments table, and the app code holds no authorisation logic to forget. The stack is the same one the generators produce (Lovable-assembled React, Supabase, Resend for email reminders, WhatsApp nudges via wa.me click-to-send with no paid API), which is exactly the point. The tools are not the problem. The missing policy review is.
Finding 2: 14 databases were writable with the public anon key
Chart 2: Key and auth failure modes
Count of 25 databases affected. Categories overlap; one database can appear in several rows.
| Failure mode | Databases (n) | Share of 25 |
|---|---|---|
| Anon key with effective write access to at least one user-data table | 14 | 56% |
| No rate limiting on auth endpoints or Edge Functions | 22 | 88% |
| Email confirmations disabled (auto-confirm) in production | 12 | 48% |
| Service-role key reachable from client code or a public repo | 7 | 28% |
| Auth site URL still pointing at localhost | 6 | 24% |
Sources: 25 anonymised audit engagements through Appycodes (2025-2026); client bundle and repository scans; Supabase auth configuration exports; scripted PostgREST role probes.
The 14 anon-writable databases are the 9 with RLS disabled plus 5 of the 11 with broken policies. This is worth stating plainly because founders consistently misunderstand it: the anon key is supposed to be public. Every Supabase app hands it to every visitor. It is only a catastrophe when RLS underneath it is missing or permissive, at which point the key printed in your page source is a master key to the database.
The service-role key is a different animal: it bypasses RLS by design and must never leave a trusted server. We found it in 7 of 25 projects somewhere a stranger could reach it: 4 in client bundles (the generator wired an admin feature directly from the browser), 2 committed to public repositories, and 1 leaking through an Edge Function's error response. The auto-confirm and localhost findings are quieter, but auto-confirm plus no rate limiting is an open invitation to bot signups, and a localhost site URL silently breaks every password reset email the day you go live.
A related habit sits just outside this chart but belongs in the same conversation: secrets in source. Several projects in the sample had webhook signing secrets, Resend API keys, or payment provider credentials committed directly into the repository rather than held in environment variables, usually because the generator was asked to make the webhook work and took the shortest path. Key rotation fixes the past; moving every secret into the environment, and treating the repo as public even when it is not, is what prevents the sequel.
Finding 3: 21 of 25 databases carried index debt on foreign keys
Chart 3: Index debt by generating tool
Foreign-key and hot lookup columns without a covering index, cross-referenced with pg_stat_statements.
| Tool | Sample (n) | Databases missing at least one FK index | Median missing indexes per database |
|---|---|---|---|
| Lovable | 11 | 10 | 8 |
| Bolt | 6 | 5 | 7 |
| Cursor | 5 | 3 | 4 |
| v0 | 3 | 3 | 6 |
Sources: 25 anonymised audit engagements through Appycodes (2025-2026); pg_stat_statements snapshots; Supabase dashboard query performance exports.
Postgres does not index foreign-key columns automatically, and none of the generators do it for you either. The result is a schema that behaves perfectly in the demo (hundreds of rows) and falls over quietly in production (hundreds of thousands). In 17 of the 25 databases, the top statements in pg_stat_statements were dominated by sequential scans that a single index would remove. The median database was missing 7 covering indexes; the worst was missing 23. This is the cheapest finding in the study to fix: the audit query below plus an afternoon of create index concurrently typically clears it, and the anatomy engagement's 1,340ms to 74ms p95 improvement came from exactly two composite indexes.
select c.conrelid::regclass as table_name,
a.attname as fk_column
from pg_constraint c
join pg_attribute a
on a.attrelid = c.conrelid
and a.attnum = any (c.conkey)
where c.contype = 'f'
and not exists (
select 1
from pg_index i
where i.indrelid = c.conrelid
and i.indkey[0] = a.attnum
)
order by 1, 2;Finding 4: zero of 25 had ever tested a restore
Chart 4: Operational posture at intake
Backups, environments, and billing controls as we found them on day one of each audit.
| Posture item | Databases (n) |
|---|---|
| Restore from backup tested before our audit | 0 of 25 |
| Pro plan with automated daily backups in place | 12 of 25 |
| Free tier with no automated backups at all | 13 of 25 |
| Single project serving both development and production | 19 of 25 |
| Spend cap off with no billing alerts configured (of the 12 Pro projects) | 7 of 12 |
Sources: 25 anonymised audit engagements through Appycodes (2025-2026); Supabase dashboard exports; Supabase usage and billing pages; engagement intake notes.
Backups you have never restored are a hypothesis, not a safety net. Twelve projects had daily backups because the Pro plan includes them; none of the twelve owners had ever run a restore, so none knew whether it worked or how long it took. Thirteen were on the free tier, where there are no automated backups at all: for those products, a single destructive migration was the end of the company's data.
What a restore drill actually involves, since none of the 25 owners had seen one: spin up a scratch project, restore the latest backup into it, run the application's smoke tests against the restored data, and write down the wall-clock time. Across the drills we ran during these engagements the median was about half an hour of elapsed time, which is also the honest answer to the question every founder asks afterwards: how long would we be down? For products where losing a day of writes is unacceptable, the conversation moves to point-in-time recovery, but PITR on top of untested daily backups is buying a better parachute before checking the first one opens.
The environment finding deserves its own sentence: 19 of 25 founders were still prompting their AI tool against the production database. That is the normal generated-app workflow, the tool connects to one project and keeps it, and it means every schema experiment, every regenerate-this-page whim, runs against live customer data. Of all the fixes in our standard hardening pass, splitting dev from prod is the least glamorous and prevents the worst single day.
Finding 5: the median database bills 12x at 10x traffic
Chart 5: Billing Bomb Index (BBI) by cohort
Modelled monthly bill at 10x current traffic vs current spend. Free-tier projects floored at the $25 Pro base, since none survive 10x on the free tier.
| Cohort | Sample (n) | Median spend today | Modelled at 10x traffic | Median BBI |
|---|---|---|---|---|
| Storage-heavy (user uploads, original-size images) | 9 | $25 | $610 | 24.4 |
| Egress-heavy (public buckets, no CDN caching) | 7 | $25 | $340 | 13.6 |
| Compute and database bound | 9 | $25 | $180 | 7.2 |
Sources: 25 anonymised audit engagements through Appycodes (2025-2026); Supabase usage and billing pages; GA4 traffic baselines; Supabase public pricing.
Compute is not what blows up. Storage and egress are. The generators wire file uploads the obvious way: a public bucket, no size limit, no MIME restriction, original files served raw. Users upload 8MB phone photos, the app displays them as 300px thumbnails, and every page view pays full-resolution egress. Across all 25 databases the median Billing Bomb Index was 12.1; the worst case was 41, a storage-heavy project whose modelled 10x month came to just over $1,000 against a $25 baseline. Ten of the 25 had public buckets with unlimited upload, and 24 of 25 had no billing alert configured at all.
The spend cap deserves an honest paragraph, because it is usually described as the fix and it is really a trade. Cap on, a traffic spike degrades and throttles your project: the billing bomb becomes an availability bomb, and your best day of press coverage is the day the app goes down. Cap off, the project stays up and the invoice arrives. Seven of the twelve Pro projects had switched the cap off, most after a throttling scare, and none of the seven had configured the billing alerts that make cap-off survivable. Either setting is defensible. Holding it by default, without alerts, is not.
How we score an AI-built Supabase database
Three metrics, computed the same way for every database in the sample, so that a founder can hear a single number per axis instead of a 40-row spreadsheet. They deliberately measure different failure economics: RCR is about data exposure, IDS about performance at scale, BBI about cost at scale. A database can score well on two and catastrophically on the third, though in this sample most managed to fail at least two at once.
1. RLS Coverage Ratio (RCR)
RCR = Tables with correct, tested RLS policy pairs / Tables holding user or client data
Correct means probed, not read: our harness attempts the forbidden operation for each role and asserts failure. Median RCR across the sample was 0.31. The five clean databases scored 1.0; the worst scored 0, with every user-data table open. Anything below 1.0 on tables holding client data is, in our view, not launchable.
2. Index Debt Score (IDS)
IDS = FK and hot lookup columns without a covering index / FK and hot lookup columns total
Median IDS was 0.58: more than half of the columns that should carry an index did not. Unlike RCR, index debt is invisible to users at prototype scale and fully mechanical to fix, which is why we score it separately rather than folding it into a single health number. A database can be perfectly secure and still unusable at 50k rows.
3. Billing Bomb Index (BBI)
BBI = Modelled monthly bill at 10x current traffic / Current monthly spend (floored at the Pro base)
The 10x multiplier is deliberate: it is roughly one good launch, one viral post, or one seasonal spike. A BBI near 10 means costs scale linearly with traffic, which is fine if your revenue does too. The storage-heavy cohort's median of 24.4 means costs scale more than twice as fast as traffic, which is a unit-economics problem wearing an infrastructure costume.
What 25 audits actually taught us
- Enabled-but-wrong RLS is more dangerous than disabled RLS. Disabled shows up in any checklist scan and scares people into action. A
using (true)policy shows a green shield in the dashboard and survives until someone probes it. 11 of our 25 were in that state, and none of their owners knew. - Each generator has a consistent failure fingerprint. The Lovable databases in our sample tended to ship auto-confirm auth and localhost site URLs; the Bolt ones favoured broad authenticated-role policies; the Cursor ones had the leanest schemas and the least index debt. Small samples, so treat these as observations rather than league tables, but the practical point stands: if you know which tool built the backend, you know where to look first.
- Index debt is the cheapest large win in the entire rescue pipeline. One query to find the gaps, one afternoon to close them, and p95 improvements measured in multiples rather than percent. Nothing else in this study has that ratio of effort to effect.
- Billing bombs come from storage defaults, not from compute. Every modelled blow-up in our sample traced to buckets and egress. The fix is boring: size limits, image transforms, CDN caching, billing alerts. No architecture change required.
- The five clean databases all had the same thing in common: a human had reviewed the model's output. Three belonged to founders with engineering backgrounds, two had been through an earlier (partial) professional review. The dividing line in this dataset is not which AI tool you used. It is whether anyone who understands Postgres ever looked at what it produced.
Recommendations
If you are pre-launch on a generated backend
Run the same checklist we run. This is the 12-item prototype-vs-production gap from our Supabase development practice, and it is the shape of almost every engagement in this study: the generator reliably delivers the first three rows and reliably skips the other nine.
| Item | Typical prototype | Production |
|---|---|---|
| Tables created | Yes | Yes |
| Basic auth working | Yes | Yes |
| Working UI | Yes | Yes |
| RLS policies on every table | No | Yes |
| Indexes on join + lookup columns | No | Yes |
| Backup strategy + restore tested | No | Yes |
| Environment separation (dev/staging/prod) | No | Yes |
| Webhook secrets in env, not source | No | Yes |
| Error logging + alerting | No | Yes |
| Rate limiting on Edge Functions | No | Yes |
| Soft deletes where data matters | No | Yes |
| Audit columns (created_by, updated_at) | No | Yes |
If you only do one thing before launch, do the two-minute anon-key probe from the anatomy above. Open your deployed app, copy the anon key and project URL from the network tab (they are in every request your app makes), and from a terminal attempt a select, an insert, and an update against each table that holds user data, first with no session and then with a throwaway account's session token. Every operation your product does not explicitly offer should fail. It costs nothing, requires no access you don't have, and it is the single test that separated the 5 clean databases from the other 20.
If you are already live
Fix in order of blast radius, not convenience: data exposure first (RLS, key rotation), data loss second (backups, environment split), money third (buckets, alerts, the cap decision), speed last (indexes). Rotating the JWT secret invalidates active sessions, so schedule it, but do not postpone it: every day a service-role key sits in a public bundle is a day your database has no security model at all. If the backend is one part of a larger unfinished prototype, our AI app completionengagement runs this database pass alongside the application-layer work rather than as a separate project. And one reliability note from the field that has nothing to do with your configuration: if your Indian users report the app simply not loading on JioFiber or Jio mobile data while working fine on Airtel, that is Jio's ISP-level DNS filtering of Supabase domains, and we published the diagnosis and four production-grade fixes separately.
If you are raising, or being acquired
Assume technical due diligence will run some version of this audit, because increasingly it does: our Series A codebase audit study found investor-side reviews now routinely probing RLS on Supabase-backed products. A failed anon-key test in a data room is expensive in ways an invoice never is. For products heading from generated prototype toward a funded, multi-tenant SaaS, our AI SaaS product development engagement builds the subscription, tenancy, and admin layers on top of a database that has already passed the checks above.
Limitations
This is a rescue-pipeline sample, and rescue pipelines select for trouble: founders whose databases were fine mostly never called us, so the 20% clean rate is likely pessimistic for AI-generated databases at large. The sample is small (25), and the per-tool splits are smaller still (3 for v0), so tool-level comparisons are directional at best. The BBI figures are models, not observed invoices: they assume usage scales linearly with traffic and use the public price sheet, and real spikes are lumpier than that. Generators are also improving quickly; this dataset spans mid 2025 to mid 2026, and the newest Lovable and Bolt output we have seen ships better RLS scaffolding than the older projects in this sample. Finally, we sell the fix, so read our incentives alongside our findings. The raw checks are all reproducible from this post; run them yourself before you pay anyone.
The pattern across all 25 is the same one we found in the application layer: AI tools compress the first 80% of building a product and quietly skip the 20% that carries the risk. The database is where that skipped 20% costs the most, because it is where the customer data lives. The companion studies:
Research
We Audited 31 Lovable / Bolt / v0 / Cursor Codebases. Here's What Survives Production.
A code-level teardown of 31 AI-generated SaaS prototypes: three proprietary metrics (PSR, TDR, RCM) and a 10-mode failure taxonomy.
Research
Lovable / Bolt to Production: The Real Cost & Timeline (20 Engagements, 1 Anatomy)
Opens with a deep teardown of one specific AI-prototype-to-production engagement, then aggregates cost and timeline across 20 projects.
Research
Series A Code Audit: Inside 23 Funded SaaS Codebases
Patterns from 23 SaaS codebase audits: opens with one anonymised takeover, then aggregates the rubric findings. TDS, KPC, MTS.
The engagements behind this dataset, from the fixed-scope database audit to the full prototype graduation:
Service
Supabase Development
RLS, multi-role auth, and production-hardening for AI-built apps. The fixed-scope audit in this study is this engagement.
Service
AI App Completion
Take an AI-built prototype to a production-ready product: the database pass above plus the application layer around it.
Service
AI SaaS Product Development
Multi-tenant AI SaaS with subscriptions and admin dashboards, built on a database that passes every check in this post.
Frequently asked questions
- Is my Lovable app's Supabase database safe to launch?
- Statistically, probably not: 20 of the 25 AI-generated databases we audited failed on RLS alone. The two-minute test: take the anon key from your own browser bundle and try to insert a row into a user-data table with it. If the insert succeeds, the database is not safe. A full audit covers RLS correctness, key exposure, indexes, backups, and billing exposure.
- What does a Supabase production audit cost?
- Our fixed-scope audit runs $1k to $2.5k and takes 2 to 4 days: every table tested against every role, index debt measured from pg_stat_statements, backup and billing posture documented. Remediation is usually $3k to $8k on top depending on table count and how much of the access model needs redesigning. The rescue described at the top of this study was $4,500 all-in.
- Should AI-built apps use Supabase or Firebase?
- Stay on Supabase. It is what the generators default to, and it gives you real Postgres: SQL, RLS, views, triggers, and a clean exit path to self-hosted Postgres later. Firebase has the exact same class of failure (permissive security rules shipped by default), so switching does not remove the problem, it just swaps the syntax. Fix the RLS you have.
- Do I need RLS if I have an API layer in front of Supabase?
- Yes. Supabase exposes your database directly over PostgREST, and the anon key that ships in your client bundle can talk to that endpoint whether or not your API layer exists. An API in front does not close the direct path. On Supabase, RLS is the security model, and anything else is decoration.
- How do I stop a Supabase billing bomb?
- Four controls: put size limits and MIME restrictions on every storage bucket, serve images through the transformation API instead of raw originals, keep public buckets behind a CDN with caching, and configure billing alerts before you switch the spend cap off. The spend cap itself is a trade: on, a traffic spike throttles your project; off, it bills you. Choose deliberately, not by default.
