Build notes14 min read

The 12 security gaps I check first in an AI-built app

Moltbook left row-level security off behind a public Supabase key, and 1.5 million API tokens were readable. These are the 12 security gaps I check first in an app built with AI tools, from database access rules to secret keys in your JavaScript, and how to test each one yourself.

Written by
Veer SinghFounder
Published
Reading time
14 min
Filed under
Build notes
Old wooden doors left open to a courtyard, with a chain and padlock hanging unused on one side
Photo: Zoshua Colah / Unsplash

Moltbook's Supabase key was in its website's JavaScript when Wiz's researchers looked. That's normal for a Supabase app. What wasn't normal, according to Wiz's February write-up (opens in a new tab), was that row-level security hadn't been set up behind it. Anyone holding the key could read and write the production database, 1.5 million API tokens and 35,000 email addresses included.

Wiz quotes the founder: "I didn't write a single line of code for @moltbook."

Apps built with Lovable, Bolt, v0 or Cursor usually run fine. When they're unsafe, it tends to be in the same few places: database tables nobody locked, secret keys shipped to the browser, API routes that never ask who's calling, and payment webhooks nobody verifies. You can check all four yourself before you launch or take a payment.

We use AI coding tools on our own projects too, and our rule, from how we build, is that AI drafts and senior engineers decide auth, roles and secrets. In June I wrote about where AI app builders stop. This note stays on security: 12 gaps, and how to test each one yourself.

Is my AI-built app safe? A 10-minute self-check

Test only your own app.

  1. In Supabase, open Advisors, then Security Advisor. Any table flagged for missing row-level security is open.
  2. Make two accounts. Can account B open account A's order or profile by pasting its URL?
  3. Logged out, in a private window, try the pages and buttons that should need a login.
  4. Search your live site's JavaScript for secret keys. Gap 4 shows how.
  5. Confirm your payment webhook checks the Stripe signature before doing anything.
  6. Put a spending cap or billing alert on every paid API.
  7. Find out what your database plan backs up, and whether anyone has restored one.

If an answer is "I don't know", count it as a gap. If you only do one thing this week, run the two-account test (step by step under gap 1). The Production Gap Checklist covers the rest of what production needs.

Why does AI-generated code have these security gaps?

The model builds what you asked for, and nobody asks for the negative space. You ask for an orders page. You don't ask it to stop one seller reading another seller's orders.

Tenzai, a security company, ran the most useful test I've read. In December 2025, five coding agents (Cursor, Claude Code, OpenAI Codex, Replit and Devin) each built the same three apps, and Tenzai's "Bad Vibes" write-up (opens in a new tab) counts 69 vulnerabilities across the 15. Not one was an exploitable SQL injection or XSS bug, the classics that frameworks now protect against. The serious ones were in logic that depends on context, like who owns an order or what a price may be.

So most of the list below is about permission. Better prompts help at the edges, but a person still has to decide the rules.

Who can read and change your data?

1. Supabase row level security that's off, or on and wide open

Row level security (RLS) is Postgres checking, row by row, whether the person asking may see or change that record. It matters on Supabase because the browser talks to the database directly with a public key.

A wooden card catalog with one drawer pulled open, its index cards within reach of anyone passing by
Photo: Diego Marín / Unsplash

Supabase's row-level security docs (opens in a new tab) say a table in an exposed schema without RLS can be read and written by any role with a grant on it. Once RLS is on, the public key reaches nothing until you write policies. So the visible key is fine. The table without RLS is the leak.

If your builder wrote the SQL, don't assume RLS is on. Supabase's security retro (opens in a new tab), published in January, says tables created in the dashboard get RLS by default, while tables created by migrations or outside tools don't unless their SQL turns it on. It also says Supabase emails the project's owners when a table is created with RLS off, so check that inbox.

And on isn't the same as protected. The RLS docs point out that a policy of to anon using (true) gives every logged-out visitor read access to every row. Written to authenticated, it opens every row to every logged-in user.

Then ask what each policy trusts. Signed-in users can edit their own user metadata, the docs note, so a policy that reads someone's role from it lets people promote themselves.

Fixed looks like RLS on every table in the public schema, with ownership policies such as using ((select auth.uid()) = user_id), so each person reads only their own rows.

Testing your RLS the way a stranger would

The Security Advisor finds tables with RLS switched off. This test finds policies that are on but wrong. You need two test accounts, A and B, a terminal, and your own project, nobody else's.

Log in as A and create something private, like an order, with a word in it you'll recognize, such as PINEAPPLE.

Now ask for that table the way a stranger would, with nothing but the public key. The key and your project URL are in the dashboard's API settings, and in your site's JavaScript. This request comes from Supabase's API quickstart (opens in a new tab), with your details swapped in:

curl 'https://YOUR-PROJECT.supabase.co/rest/v1/orders?select=*' -H "apikey: YOUR-PUBLIC-KEY"

For a private table you want an empty list, [], or a permission error. If PINEAPPLE comes back, anyone on the internet can read that table. Repeat it for every table, not only the sensitive ones.

Then test as B. Log in as B in Chrome, open developer tools, and type rest/v1 into the Network tab's filter box. Click around, right-click one of the requests that appear, and choose Copy, then Copy as cURL (opens in a new tab), which carries B's login token with it.

Paste it into the terminal and edit the address to end with the table name and ?select=*, which asks for every row. B should get B's rows and nothing else. If PINEAPPLE shows up, that table's policy isn't checking who owns what.

Reading is only half. Moltbook's database was writable too, so ask whoever built the app to repeat the B test with an update to one of A's rows.

If a terminal isn't your thing, the dashboard can run a version of this. Supabase added user impersonation to its Table Editor (opens in a new tab) in December 2023: pick anon to see a table as a logged-out stranger would, or authenticated and a specific user to see it as B, with your policies applied.

2. Changing an ID shows someone else's data

Your app loads /invoices/1042, someone types 1043, and the server checks only that they're logged in, not that the invoice is theirs. Security people call this broken object-level authorization, or IDOR. It's first on OWASP's 2023 API Security Top 10 (opens in a new tab), which calls it "extremely common in API-based applications."

A demo never shows it, because in a demo you're always the owner. One app in Tenzai's study had an order API that "checks if shoppers are viewing their own orders, but completely skips this validation for users with any other role."

Then run the same test through the app: with two browsers, open a record as A and paste its URL into the other, logged in as B. Every read and write should be authorized on the server, whatever the interface hides.

3. Rules that only exist in the browser

A hidden admin button doesn't protect the admin page. Neither does a paywall that checks your subscription in the browser, or a checkout that takes the price from the page. Sebastian Markbåge's 2023 post on Next.js security (opens in a new tab) says a URL parameter "should not be used to track things like ?isAdmin=true".

Tenzai saw the money version too: when the prompt didn't say quantities must be positive, four of the five tools didn't check, and three allowed a negative price.

From a free account, open a paid feature's URL, and ask whether checkout looks up prices on the server. Only a verified payment event (gap 7) should change who has paid. And don't rely on Next.js middleware alone to protect a page; check the user again where the work happens.

Which API keys are safe to expose in the browser?

4. Exposed API keys in the JavaScript bundle

Every file your site sends to the browser is readable by every visitor. The Next.js post notes that any environment variable prefixed NEXT_PUBLIC_ is exposed to the client. Vite's docs (opens in a new tab) say the same of VITE_ variables: their values are "bundled into your source code at build time," so they shouldn't hold API keys.

Some keys are made for that. Supabase's public key is one, and so is a Stripe key starting pk_; Stripe's docs (opens in a new tab) say publishable keys are the only ones safe to expose outside your backend. Your Stripe secret key and your OpenAI key aren't. A leaked OpenAI key lets a stranger make requests you pay for, and a Stripe secret key lets them act on your Stripe account.

Searching your own JavaScript, step by step

  1. Open your live site in Chrome, not the builder's preview, and log in. Visit the main pages, admin and paid ones included, since some code only loads with its page.
  2. Open developer tools and press Control+Shift+F, or Command+Option+F on a Mac. That opens a search across every file the page has loaded (opens in a new tab).
  3. Search, one at a time, for sk_live_ and rk_live_ (Stripe's secret and restricted keys), sk-proj- (OpenAI), and sb_secret_ and service_role (Supabase).
  4. Older Supabase keys, public and secret alike, start with eyJ. To find the secret one, copy the last dozen characters of your service_role key from the dashboard and search for those.
  5. Last, open your hosting dashboard's environment variables. Treat every value under a NEXT_PUBLIC_ or VITE_ name as public, whether or not the search found it.

A hit on pk_live_ or on your Supabase public key is fine. A hit on anything else means rotate first: create a new key, switch the server to it, then delete the old one. Moving the code without rotating leaves the old key working.

Leaked keys don't expire on their own. In its State of Secrets Sprawl 2026 (opens in a new tab), published in March, GitGuardian retested credentials it had confirmed as valid in 2022 and found more than 64% still valid in January 2026. Treat anything pasted into a builder's chat or committed to a repository as exposed too.

5. The service-role key in the wrong place

This one undoes gap 1. Supabase's secret key, called service_role on older projects, runs as a Postgres role that bypasses RLS, and the docs say never to use it in the browser or expose it to customers. When a policy blocks a query, the tempting shortcut is to swap in that key. The error goes away for exactly the reason it's dangerous.

Search the whole project, case-insensitively, for service_role and sb_secret_. The key belongs in server code only, never under a public prefix or inside a mobile app.

Are your API routes and server actions protected?

6. Endpoints that don't check who's calling

Every API route, server action and edge function is a public door, whether or not a button points at it. The Next.js post says "use server" "exposes an end point that makes all exported functions invokable by the client", so each action "should always start by validating that the current user is allowed to invoke this action."

In another app from the study, a delete endpoint ran its ownership test only for logged-in users. For anonymous callers, "the test was skipped and the file was deleted."

Testing every route as the wrong user is an engineer's job. Yours is to ask whoever built the app for the list of endpoints. If nobody can produce one, that's your gap.

7. Payment webhooks nobody verifies

When someone pays, Stripe calls your webhook and your app upgrades the account or ships the order. Stripe's webhook docs (opens in a new tab) spell out the risk: "Without verification, an attacker could send fake webhook events to your endpoint to trigger actions like fulfilling orders, granting account access, or modifying records."

Verifying means checking the Stripe-Signature header with your whsec_ signing secret, against the raw request body, before anything else. Stripe also warns that endpoints "might occasionally receive the same event more than once", so the handler should record the event IDs it has processed. Ask your developer to show you both.

8. No rate limits and no spending caps

In Tenzai's study, "every application included a login page with zero rate limiting or account lockout mechanisms." That leaves passwords open to guessing, and anything you pay for per call (login texts, emails, AI model calls) open to a loop.

OWASP's 2023 list (opens in a new tab) describes a script that makes a password-reset endpoint send tens of thousands of paid texts, costing "thousands of dollars in a matter of minutes." It advises spending limits on every paid service, or billing alerts where a limit isn't offered.

Enter 20 wrong passwords, press "resend code" ten times, and see whether anything stops you.

9. Uploads that land in a public bucket

Supabase's storage docs (opens in a new tab) say a public bucket skips access control for reading and serving files, so treat anything in one as readable by whoever gets the link. That's fine for product photos. ID scans and contracts belong in a private bucket behind short-lived signed links.

Upload a file as account A and open its link in a private window.

What happens on a bad day?

10. Dependencies nobody is watching

On December 3, 2025, the React team disclosed CVE-2025-55182 (opens in a new tab), a CVSS 10.0 flaw in React Server Components that allowed unauthenticated remote code execution. Next.js was among the affected frameworks; apps whose React code doesn't use a server weren't. The fix was a package update (for Next.js, next itself), with follow-up advisories after it, and an app generated once and never updated gets none of them.

Two orange life rings with white rope lying on a sunlit deck, set out before anyone needs them
Photo: Alice Kotlyarenko / Unsplash

Turn on Dependabot alerts in GitHub or run npm audit. When a tool adds a package you don't recognize, check that it exists and has a real repository before you trust it.

11. Logs full of personal data

Logging the whole request is the quickest way to debug, and if it stays in, your logs fill up with emails, tokens and addresses. Search them for @, Bearer and password, and log IDs instead of identities.

12. Backups nobody has restored

Find out what your plan backs up and whether uploaded files are included. Nobody knows a backup works until someone has restored it, so do one practice restore, somewhere it can't overwrite live data.

Are the built-in security scans enough?

Run them. Supabase's Security Advisor is built into the dashboard, and Lovable added a security scan in its 2.0 release, as Matt Palmer's statement on CVE-2025-48757 (opens in a new tab) notes. They'll catch a table with RLS switched off.

What they can't know is what your app is supposed to allow. Palmer, who reported that CVE, said the early Lovable scanner "merely checks for the existence of any RLS policy, not its correctness or alignment with application logic."

Whether a coach should see another coach's clients is a question about your product, so have a person read the rules against what the app is meant to do.

What founders ask me about security

Is vibe coding safe?

Safe enough for a prototype. For anything that stores other people's data or takes their money, not on its own: the problems sit in permissions and secrets, which a working demo never tests. Veracode's March 2026 update (opens in a new tab) put syntax correctness for generated code above 95% and the security pass rate at about 55%, roughly where it was two years earlier.

My Supabase anon key is visible in the browser. Is that a problem?

Not if your row-level security is right. The anon (publishable) key is designed to be public and only reaches what your policies allow. The real problems are a table with RLS off, a policy that lets everyone through, or the secret key showing up where the anon key should be.

I turned on RLS and now my app shows nothing. What happened?

RLS is doing its job. Until a policy allows something, the public key reaches nothing, so every query comes back empty. The fix is a policy per table saying who may read and change which rows, not switching RLS off or swapping in the service-role key (gap 5).

Do I need a penetration test before launch?

If you handle payments, health, financial or ID data, yes, or at least a code review by someone who didn't build the app. Otherwise, run the self-check and the two-account test above at the very least.

Can AI fix these security gaps for me?

It can draft the fix once you've said what the rule is. Deciding the rule is the part it can't do: who owns a record, which role sees what, when a payment counts. Have someone who understands the product review every policy before it ships.

If the tests turn something up

  • Rotate any exposed key first: new key on the server, then delete the old one.
  • Turn on RLS for an open table now, even if parts of the app break until the policies exist.
  • Check your database and hosting logs for access you don't recognize.
  • Fix the pattern, then search the codebase for the same mistake elsewhere.
  • If customer data was exposed, find out what the law requires you to tell people, and by when.

You can do most of this page on your own. The harder part is judgment: which of these gaps apply to your app, and which have to close before launch. That's the conversation I have on a free Production Gap Review call. Afterward you get the findings in writing, with security set beside the other five areas we look at.

When the fixes are bigger than an afternoon, Launch Engineering is how we do them, starting with environments, auth and roles and leaving new features until those are solid.

And if everything comes back clean, write down how you tested it. You'll want the same checks after the next big change.

Your turn · Production Gap Review

Find out what’s between you and production.

Bring your prototype, or just the idea. Leave with a written gap map — deployment, security, payments, integrations, growth readiness — before you spend a dollar. If we’re not the right team to close the gaps, the map is still yours.

30 minutes. A written gap map. No pitch unless you ask for one.

info@synaptech.io +1 (925) 255-9507 Pleasanton, CA

Free Production Gap ReviewBook a call