Back to Blogs

Development7 min read

Why Your AI-Built App Works in Development but Breaks in Production

Works locally,breaks live

When an app works in the builder’s preview or on your laptop but fails once deployed, the code is rarely the whole story. Production differs from development in configuration, data, network, permissions and scale, and AI builders hide many of those differences while you work. Most “it worked yesterday” failures trace back to one of seven causes. This guide explains each one, how to confirm it, and how to stop it happening again.

Key takeaways

  • Read the server logs first. The browser shows that something failed; the logs say why.
  • Config is the usual suspect. Missing variables, build-time values and preview-only settings cause most deploy failures.
  • Production data is different data. Empty, large, or older-shaped data breaks code that passed every test on a clean database.
  • Fix the process, not just the bug. Repeatable builds, migrations and a staging environment stop the next one.

What actually changes between development and production

In development, everything is forgiving. The builder injects your keys for you, the database has a handful of rows you created, requests come from one browser, and errors appear on screen. In production, configuration comes from your hosting provider, the database holds real and sometimes messy data, requests arrive concurrently from devices you have never tested, and errors are hidden from users by default. Each difference is a place where an assumption in the code can break.

AreaIn developmentIn production
ConfigurationInjected by the builder or a local .env fileSet in the hosting provider, per environment
BuildDev server, compiled on the flyOptimised build, values frozen at build time
DatabaseSmall, clean, recently createdReal data, older rows, migrations applied over time
AuthPreview or localhost URLsYour real domain and redirect URLs
NetworkSame origin, generous limitsCORS, rate limits, timeouts, webhooks from outside
ErrorsShown on screenHidden from users, written to logs

The seven most common causes

1. Environment variables are missing or wrong

Builders such as Lovable and Bolt manage keys for you inside their environment. When you deploy elsewhere, those values have to be set again in your host’s dashboard, for the right environment. The Twelve-Factor App principle of keeping config in the environment is sound, but it means a forgotten variable produces code that compiles and then fails at runtime.

Confirm it by listing every variable your code reads, then comparing that list with what the host has. Look for typos, variables set only for “Preview” and not “Production”, and values copied with trailing spaces.

2. Frontend values are frozen at build time

Frontend frameworks inline public variables into the JavaScript bundle during the build. The Next.js documentation notes that once built, the app will no longer respond to changes to these values; Vite’s VITE_ variables behave the same way. So if you add or change a value after deploying, the live site keeps the old one until you rebuild. It also means the same build cannot be promoted from staging to production if their public values differ.

3. The database schema in production does not match the code

In a builder, schema changes often happen instantly, sometimes by editing tables directly. In production, the database has a history. If a column was added in development but the migration never ran in production, or ran against a table with existing rows that violate a new constraint, queries fail. Typical messages mention a missing column, relation or function.

The fix is to treat schema changes as code: versioned migration files, applied in order, by the deploy process, and tested against a copy of production-shaped data. Changing production tables by hand is how environments drift apart.

4. Authentication still points at the preview

Login works on the preview domain, but in production users bounce back to the preview, to localhost, or to an error page. The auth provider’s allowed redirect URLs, site URL and OAuth callback settings still reference the development address. Social login providers such as Google need the production domain registered too. Check every place a URL is configured, including email templates for confirmation and password reset.

5. Permissions that the preview bypassed

Some preview environments run with broader access than a real user has. When the deployed app uses an ordinary user session, database rules suddenly apply, and queries that returned data now return nothing or a permission error. On Supabase, this usually means Row Level Security is enabled without the policies the app needs. The Supabase guide to securing your API explains how grants and RLS policies work together. The answer is to write the correct policies, never to switch security off to make the error disappear.

6. Third-party services behave differently live

Payment providers, email services and AI APIs all have separate test and live modes with separate keys, limits and settings. Common failures: live webhooks pointing at a preview URL, webhook signing secrets that belong to the test endpoint, emails rejected because the sending domain is not verified, and AI API calls hitting rate limits under real traffic. Stripe, for example, signs each webhook with an endpoint-specific secret and retries failed deliveries for up to three days in live mode, so a misconfigured endpoint can produce a confusing stream of repeated events.

7. Runtime limits and real-world load

Serverless functions have execution time limits, memory caps and cold starts. Code that processes a large upload or calls a slow AI model can work locally and time out in production. Concurrency exposes bugs too: two users submitting at once can overwrite each other’s changes or double-charge a card if the code was never written with that in mind. These need design changes, such as moving long work to background jobs and making writes idempotent, rather than configuration tweaks.

How to debug a production failure step by step

  1. Reproduce it on the live URL with the browser’s developer tools open. Note the failing request, its status code and response.
  2. Find the matching server log entry in your host’s function logs, at the same timestamp. This is where the real error message lives.
  3. Classify the error. “Undefined” or “missing key” points to config. “Column does not exist” points to migrations. 401 and 403 point to auth or permissions. Timeouts point to runtime limits.
  4. Check what changed. Compare the last working deploy with the failing one: code, variables, database migrations and third-party settings.
  5. Fix it in a way that holds. Add the missing variable to your documented list, write the migration, or add a test for the case that broke.

Preventing the next one

A few habits remove most of these failures. Keep a written list of required environment variables per environment. Add a staging environment that mirrors production settings, so problems appear there first. Run database changes only through migrations. Add a health check that confirms the app can reach its database and key services after each deploy. And make deploys repeatable from your repository, so the live site never depends on something that only existed in the builder.

If you are preparing for a first launch, our checklist of ten things to fix before launching an AI-built app covers the security and reliability side. For the wider journey from prototype to product, see how to turn an AI prototype into a production-ready SaaS.

Sources and further reading

Keep reading

FAQs

Frequently asked questions

Usually because the variables were never added to the hosting provider, were added after the build ran, or are build-time variables that need a fresh build to take effect. Frontend variables such as VITE_ or NEXT_PUBLIC_ are baked in when you build.

Still have questions? Email our team.