Skip to main content
Reactll — AI & Software Engineering Reactor Technology
For Manufacturing & Export For Logistics & Freight For Restaurants & Cafes For Events & Tourism For Real Estate For E-Commerce & Retail For Law, Tax & Accounting Firms For SaaS & Startups
Case Studies Pricing

Your Lovable, Bolt or Cursor app has paying users: what breaks first

9 min read
Share this post

In short

The nine weak spots we find in AI-built apps once real users and payments arrive, why each one happens, and how to check it yourself today.

A tool like Lovable, Bolt or Cursor can get you from an idea to a working app in a weekend. Then people start paying, and the questions change. It no longer matters whether a feature works on your account. It matters what happens when a stranger, a bot or a second customer uses the app in ways you never tried.

We review and fix AI-built apps, and we write our own products with AI coding agents every day. The weak spots are predictable. They are rarely in the screens you can see. They sit in the places the AI was never asked about: who is allowed to read which data, which keys the browser can see, and what happens when something fails at 3 a.m.

Below are the nine things that break first, roughly in the order they can hurt you. For each one: why it happens, how to check it yourself, and what fixed looks like.

1. A secret key is sitting in the browser

Anything the browser downloads, anyone can read. Open your live app, open the browser's developer tools and search the loaded JavaScript files for strings like sk_live, service_role, sb_secret or sk-. Bots scan public site code for exactly these patterns.

This happens easily in AI-built apps. In Vite projects, any setting whose name starts with VITE_ is copied into the browser code. In Next.js the same goes for NEXT_PUBLIC_. Ask the AI to "make the Stripe call work" and it may put the secret key wherever the call happens to be.

What fixed looks like: the browser only ever holds public keys (a Stripe publishable key, a Supabase anon or publishable key). Anything with real power runs on a server or a serverless function. Any key that was ever public gets rotated, meaning replaced with a new one. Deleting it from the code is not enough, because it stays in the git history.

2. Users can read each other's data

This is the most serious one, and you cannot see it by clicking around as yourself. Many of these apps run on Supabase, where the browser talks to the database directly with a public key. That is safe only if row-level security (RLS) is on. RLS is a set of database rules that decide which rows each logged-in user may read or change.

The Supabase documentation on row-level security says RLS should be enabled on every table in an exposed schema. Once it is on, the public key cannot read anything until you write policies, the rules that grant access. It also says the secret or service_role key bypasses RLS for requests that carry no user login, so that key must never leave the server.

How to check: create a second test account. Log in as user B and try to open user A's records, by changing an ID in the address bar or in a request in the developer tools. Then check each table in Supabase: is RLS on, and does each policy compare the row's owner to the logged-in user? A policy that simply allows everything is the same as no policy.

The same applies outside Supabase. Every API route has to check on the server who is asking. Hiding a button is not access control.

3. Payments trust what the browser says

A common AI-built checkout works like this: the browser sends the price, or the app marks a user as "paid" when they land on the success page. Both can be faked by anyone who can edit a request or type a URL.

What fixed looks like: the server decides the price from its own product list. The browser only says which product. Access is granted when the payment provider confirms the payment to your server, not when the user reaches a thank-you page. Upgrades, refunds and cancelled subscriptions all change access on the server too.

4. Webhooks are not checked

A webhook is an automatic message one service sends another, such as "this payment succeeded". If your webhook address accepts any message, anyone who finds it can post a fake "payment succeeded" and unlock your product.

Stripe's webhook documentation is clear about three things. First, verify the signature in the Stripe-Signature header, using the unchanged raw request body. Second, the same event can arrive more than once, so record the event IDs you have handled and skip repeats. Third, events do not always arrive in order. An app that grants a month of access every time it sees an event will grant two months when the event is sent twice.

How to check: find the webhook route in the code and look for the signature check. Then ask: what happens if this exact message arrives twice? Other providers (Paddle, Lemon Squeezy, email and SMS services) sign their webhooks in a similar way.

5. Nothing limits how often someone can call you

Without rate limits (caps on how many requests one user or IP address can make in a given time), one script can try thousands of passwords, create thousands of fake sign-ups, or flood your password-reset emails. If your app calls a paid AI model, a bot can run up a bill you only see at the end of the month.

What fixed looks like: limits on login, sign-up, password reset, contact forms and every AI feature, plus a hard spending cap with the AI provider. In our own product Estimonia, which values items from photos and works without an account, every AI call is logged with its cost. Limits per IP address and per user protect the parts that are open to everyone. That is the minimum for any feature a stranger can trigger.

6. Login works on the happy path only

Sign-up and login usually work in the demo. The edge cases are where they break:

  • A password-reset link that never expires, or keeps working after it has been used.
  • A user changes their email, and the old address can still log in.
  • A deleted or banned user keeps a valid session.
  • Admin pages hidden from the menu but reachable by typing the URL, with admin API routes that never check the role.
  • Email confirmation that can be skipped by calling the API directly.

Test each of these with real accounts. They take minutes to check and are easy to miss when the AI wrote the flow and nobody read it.

7. There are no backups, and the database changes by hand

Ask two questions. Is there an automatic backup? Has anyone ever restored one? A backup you have never restored is a hope, not a backup. Check what your hosting plan actually includes and how far back it goes.

The second problem is how the database changes. Prompt-driven apps often change tables directly in a dashboard. Then nobody can say what the live database looks like, or rebuild it. Migrations fix this. A migration is a small file in the code that records each change to the database, so every change is versioned, reviewable and repeatable.

8. Errors happen and nobody hears about them

If an email fails, a background job crashes or a payment webhook returns an error, does a person find out? In many AI-built apps the answer is "when a customer writes in". Error monitoring means every server error and browser error is collected in one place someone actually looks at, with an alert for the ones that matter.

Failures should also be planned for. Background tasks should retry, and give up loudly. In Humainbox, our product that screens contact-form messages arriving by inbound mail webhook, every decision is saved with its reasons. If the filter cannot reach a decision, the message is delivered rather than lost. Decide on purpose what each failure should do. Otherwise the code decides for you.

9. Prompts go straight to the live app

"Every new prompt fixes one thing and breaks two others" is the most common thing we hear. It gets dangerous when each prompt changes the live app that paying users are on. A staging environment is a private copy of the app, with its own database and its own keys. Changes go there first and are checked before they reach production.

What fixed looks like: the code lives in a git repository you own. Production and staging use different keys and different databases. Changes reach production through version control, ideally with a few automatic tests around signing up, paying and the core feature. You can keep prompting. The prompts just stop landing directly on your customers.

A ten-minute start

If you only have a few minutes today, do these three things:

  1. Search your live app's JavaScript for secret keys, or run our free security check. It reads only what any browser can already see: the page, its scripts and its security settings.
  2. Log in as a second user and try to open the first user's data.
  3. Find your payment webhook and confirm that it checks the signature.

The browser-side check has a hard limit, on purpose. It cannot tell you whether your database rules really separate users, or whether a webhook can be faked. Testing that needs access to the code and accounts, with your permission.

When to bring someone in

If you have real users and you cannot answer the questions above with confidence, that is the point to get the code read by someone who did not write it. That is what our vibe-coded app rescue does. It starts with a fixed-price system review, usually within a week. We read the code, the database rules, the hosting and the integrations, run these checks for real, and write down what is broken, what is fragile and what is fine. Exposed data, leaked keys and unchecked payments get fixed first, usually in days. A full rewrite is rarely needed. If it would genuinely be cheaper, the review says so.

FAQ

Frequently asked questions

Yes, the anon or publishable key is meant to be public. It is only safe if row-level security is switched on for every table that holds user data, with policies that limit each user to their own rows. Without that, the public key can read far more than it should. The service_role or secret key is different: it bypasses row-level security and must never appear in browser code.
Partly. An AI agent can add a signature check or a rate limit when you ask for exactly that. The hard part is knowing what to ask for and confirming it works. Prompting "make it secure" rarely does. Someone still has to test with a second account, replay a webhook and read the database rules. Many of these gaps are invisible from the screens the AI was working on.
Usually not. Most AI-built apps have a sound overall shape with a few dangerous gaps: keys in the browser, missing database rules, unchecked payments. Those can be fixed in place. A rewrite makes sense only when the code cannot be made safe at reasonable cost, and that should be decided after someone reads the code, not before.
Only what a browser can see: secret keys left in public JavaScript, missing security headers, cookie settings, error details left on, outdated software versions. It cannot see whether your database rules separate users, whether webhooks are verified, or whether payments are checked on the server. Those need access to the code and accounts, with your permission.

Tagged

Vibe Coding App Security Supabase Lovable SaaS

Questions this post did not answer?

Tell us about your own setup. An engineer will tell you what applies to you, not to a generic case.