Published on

Taking Your Lovable App to Production: The Complete Guide

  • Beka Makharoblishvili
    Beka Makharoblishvili
    Founder and Product Engineer

Your Lovable app works. You clicked through it, the flows do what they should, and you have shown it to people who nodded. So the natural question is why anyone would need a guide to take it further.

Because "it works" and "it survives real users" are different claims, and only one of them has been tested.

A prototype is a demonstration that the idea is possible. Production is a promise that the thing keeps working when nobody is watching it, when someone hostile pokes at it, and when a hundred people use it at once instead of one person following a happy path. Nothing about generating an app with Lovable, v0, Bolt, or Replit is a shortcut around that promise. Those tools compressed the part that used to take three weeks. They did not touch the part that takes the other three.

This guide walks the actual gap. Not "add tests and monitoring," which is advice you could get anywhere, but the specific things that break in AI-generated apps, in roughly the order they hurt.

The shape of the problem

AI app builders generate code that satisfies the prompt. That is the whole objective function. If you asked for a dashboard where users can see their invoices, you will get a dashboard where users can see their invoices, and it will be true that users can see their invoices.

What the prompt did not say, and what the generated code therefore did not consider, is whether user A can see user B's invoices. That was never in scope. It was not a mistake in the sense of a bug, it is an absence, and absences do not announce themselves. Your app has no error message for the security model nobody specified.

This is why the failure mode is so consistent across builders. The generated code is usually competent line by line. It is the unstated requirements that are missing, and the unstated requirements are exactly the ones that matter in production: authorization, data isolation, secret handling, failure behaviour, and cost control.

So the work is not "fix the bad code." It is mostly "add the things nobody asked for."

Stage one: find out who can see what

Start here, always. Data leaks are the only category on this list that can end the company rather than annoy you.

Row level security is the big one. If your app uses Supabase, which most Lovable apps do, your data lives in Postgres tables that are reachable directly from the browser through the Supabase client. That is by design and it is fine, but it means the database itself has to enforce who can read which rows. That enforcement is row level security, RLS, and it is off by default on new tables.

With RLS off, any authenticated user holds a key to the whole table. Not the whole table as your UI presents it. The whole table. Someone who opens the browser console and calls the client directly gets every row.

Check this first, on every table:

  • Is RLS enabled on the table at all?
  • Does each table have policies for select, insert, update, and delete separately? Enabling RLS with no policies denies everything, which is safe but breaks the app. Enabling it with one permissive policy is the trap: it looks configured and protects nothing.
  • Does each policy actually compare a column to the current user, rather than evaluating to true?

A policy of using (true) is the single most common thing we find. It passes every test, it makes the app work, and it means the table is public.

Then check the object level. Even with RLS correct, apps commonly expose an endpoint that takes an id and returns the record, where the id comes from the URL. If the handler fetches by id without also checking ownership, then changing /invoice/1041 to /invoice/1042 returns someone else's invoice. This is called insecure direct object reference, and it is the oldest bug in web applications. Generated code reproduces it constantly because "fetch the record with this id" is a faithful implementation of the prompt.

The test is embarrassingly simple. Log in as one user, note an id you own, log in as a different user, and request the first id. If you get data, you have found it.

Then check what the frontend is carrying. Anything in your client bundle is public. Not "hard to find." Public. Open devtools, look at the network tab, read the JavaScript. You are looking for API keys that should have stayed on a server.

The Supabase anon key is designed to be public and is fine there, which confuses people into thinking all keys are fine there. The service role key is not. It bypasses RLS entirely. If it has been used anywhere in client code, treat it as compromised, rotate it, and move that call to a server route.

Same for anything with a billing surface: your OpenAI or Anthropic key, Stripe secret key, email provider credentials. If the browser can see it, someone can spend your money with it.

Stage two: make authentication mean something

Most AI-generated apps have login. Fewer have authorization, and the difference is where the money is.

Authentication answers "who is this." Authorization answers "what are they allowed to do." Generated apps usually nail the first and skip the second, because a prompt that says "add login" produces login.

Concretely, the questions to work through:

  • If a route renders admin functionality, is the admin check on the server, or is it a conditional in the component that hides a button? Hidden buttons are not security. The endpoint behind the button is what matters.
  • Are your server routes checking the session on every request, or are they trusting that the client would not have called them otherwise?
  • Can a logged out request reach anything it should not? Try it. Log out, paste an API URL directly, see what comes back.
  • What happens on password reset, email change, and session expiry? These flows are frequently generated as UI without the corresponding server logic being correct, and they are exactly where account takeover lives.

Role checks in particular tend to be scattered. Getting them into one place, checked server side, is often the single highest value refactor in the whole engagement.

Stage three: payments, if you take money

If Stripe is in the app, three things need to be true, and generated integrations usually get one and a half of them.

Webhooks must be verified. Stripe signs every webhook. If your handler does not verify that signature, anyone who finds the endpoint can post a fake checkout.session.completed and get whatever your app grants on payment. Free subscriptions for everyone who reads your JavaScript.

Price must come from the server. If the amount is sent from the client and trusted, users set their own prices. The client should send a product identifier, and the server should look up what that product costs.

Fulfilment must be idempotent. Stripe retries webhooks. If your handler grants credits or extends a subscription every time it runs, a retry gives it twice. Key the fulfilment on the event id and ignore ones you have already processed.

None of these are exotic. They are all in Stripe's documentation. They are simply outside the scope of "add checkout to my app."

Stage four: find out what it does under load

Prototypes are tested by one person clicking slowly. That hides an entire category of problems.

Queries without indexes. Generated schemas rarely include indexes beyond primary keys. With 50 rows nothing is slow. With 50,000 rows a query that filters on an unindexed column will crawl, and because it crawls under load rather than in development, you find out during your launch.

Queries in loops. Fetching a list and then fetching a detail record for each item is the classic N+1 pattern, and it is very natural for a code generator to produce because it mirrors how the prompt described the feature. One request becomes a hundred.

Unbounded results. select * from posts with no limit is fine until it is not. Every list endpoint needs pagination before it needs it.

Serverless cold starts and connection limits. If the app opens a new database connection per request against a Postgres instance with a modest connection cap, traffic will exhaust the pool. Connection pooling is not optional at that point.

AI calls with no ceiling. If your app calls an LLM per user action and you have no rate limit, your cost scales with whoever is most enthusiastic about hitting the button. Add per user limits and a global circuit breaker before launch, not after the bill.

Stage five: decide what happens when things break

Prototypes assume success. Production code plans for failure, and this is the most consistently absent thing in generated apps.

Work through it deliberately:

  • When an external API call fails, does the user see a real message or a white screen? Is there an error boundary at all?
  • When an AI call returns something malformed, or takes forty seconds, or refuses, what does the app do? Streaming responses need timeout handling that generated code almost never includes.
  • Are errors recorded anywhere you will actually look? An app with no error tracking is an app where your users find out before you do.
  • Is there any uptime monitoring? Knowing the site is down requires something that checks.

The bar here is low and worth clearing. Error tracking and uptime monitoring can both be set up in an afternoon, and together they change your relationship with the app from hoping to knowing.

Stage six: the part everyone skips

Get the deployment boring.

That means environment variables that are actually different between development and production, rather than one .env copied everywhere. It means secrets that are not in the git history, and if they ever were, rotated, because git history is forever. It means a preview deploy so changes can be seen before they are live, and a rollback path so a bad deploy is a two minute problem instead of an evening.

It also means backups you have restored at least once. An untested backup is a belief, not a backup.

What we find most often, in order

Across the audits we run, the same handful of issues come up far more than anything else. Ranked by how often they are present, not by how bad they are:

  1. Row level security off, or enabled with a permissive policy. Present in the large majority of Supabase-backed apps we look at. The permissive-policy version is more common than fully disabled, and more dangerous, because it looks configured.
  2. No pagination anywhere. Every list endpoint returns everything. Invisible at prototype scale, and it does not degrade gracefully, it falls over.
  3. Missing indexes on the columns actually used for filtering. Primary keys are indexed, foreign keys often are not, and the queries the app runs constantly are usually filtering on the latter.
  4. Ownership not checked on single-record fetches. The id in the URL is trusted. This is the one that leaks data between customers.
  5. Secrets in the client bundle. Usually an AI provider key, occasionally a Supabase service role key, which is the serious version.
  6. No error boundaries. Any failed request takes the screen white.
  7. Stripe webhooks unverified. Present in most integrations that were generated rather than followed from the documentation.
  8. No rate limiting on AI calls. Cost is unbounded by design.

The useful thing about that list is how boring it is. These are not subtle architectural problems. They are the same eight omissions, over and over, because they are the eight things a prompt describing a feature does not mention.

That also means the work is predictable, which is why it can be quoted at a fixed price rather than hourly.

The pre-launch checklist

If you want one thing to work from, this is it. Every item is either done or not done.

Data

  • RLS enabled on every table holding user data
  • Policies for select, insert, update and delete separately, none of them evaluating to true
  • Ownership checked server side on every single-record fetch
  • Tested by logging in as two users and swapping ids

Secrets

  • No service role key, no sk_ key, no AI provider key anywhere in the client bundle
  • Anything that ever was in client code or git history has been rotated
  • Production and development use different credentials

Auth

  • Role checks on the server, not just hidden UI
  • Logged out requests to API routes return nothing useful
  • Password reset, email change, and session expiry all tested

Payments

  • Webhook signatures verified
  • Prices resolved on the server from a product id
  • Fulfilment keyed on event id so retries do not double-grant

Scale

  • Indexes on the columns the app filters by
  • Pagination on every list
  • No queries inside loops
  • Connection pooling if serverless
  • Rate limits on anything that calls a model

Operations

  • Error tracking installed and reporting
  • Uptime monitoring on the main flow
  • Error boundaries so failures degrade instead of blanking
  • A rollback path you have used once
  • A backup you have restored once

What this costs in time

Honest ranges, for a typical single-purpose app with authentication, a database, and one or two integrations.

Doing it yourself, if you are technical: two to four weeks of real work, with the security review being the part most likely to take longer than expected because you do not know what you do not know. If you are not technical, this is not a time estimate problem, it is a "you will not know whether you are done" problem, which is worse.

The reason the range is wide is that the audit is fast and the fixes are not. Finding out that RLS is off everywhere takes an hour. Writing correct policies for fourteen tables, and testing them, takes days.

If you would rather not do it, that is the work we do. We run a free teardown that tells you what is actually wrong with your app, and a Production-Readiness Audit that returns a written verdict in 48 hours: what is fatal, what can wait, a fixed quote, and a 90 day plan. If you want it fixed rather than diagnosed, the Production Sprint takes it to production in 10 business days at a published price.

The order matters

If you do nothing else from this guide, do the first stage. Check RLS on every table, test whether one user can read another user's records by changing an id, and read your own JavaScript bundle for keys that should not be there.

Those three checks take an afternoon and they cover the failures that are expensive rather than embarrassing. Slow queries make people impatient. Leaked data makes people lawyers.

Everything else on this list is real work that should happen before you have a lot of users. But the data isolation questions should be answered before you have any.

Two related pieces: if what worries you is the security side specifically, we go deeper in the founder's security checklist for AI-generated apps. If what worries you is that the app is safe but looks like every other generated app, that is a different problem with a different fix.