Guide · 6 min read

Why AI-built apps don’t scale – and how to fix it

In the projects we review, security is rarely the biggest problem. Architecture is: one file handles everything, the data sits in a SQLite file on a single server, and there is no data model for business customers. That’s fine for a prototype – and blocks everything after it.

An AI assistant builds what you ask for – feature by feature, chat by chat. That is exactly its strength, and exactly where the typical architecture of vibe-coded projects comes from: everything grows wherever there happens to be room. After a few weeks the app works, but nobody – the AI included – can change it safely any more.

The pattern: one file that does everything

Almost every project we see has one central file where everything accumulates: routing, login, business logic, database access, often even HTML. The frontend is the same – a single huge page with hundreds of kilobytes of embedded JavaScript.

This isn’t carelessness, it’s the workflow: “add an export feature” lands where the code already is. Nobody tells the AI that export should really be its own module.

The consequences show up later:

  • Every change touches everything. A new feature alters code that has nothing to do with it. Bugs appear in unexpected places.
  • Testing is barely possible. When logic, database and HTTP live in one function, nothing can be checked in isolation. That’s why most of these projects simply have no tests.
  • Two people can’t work in parallel. Everyone edits the same file.
  • The AI itself gets worse. The bigger a file, the more the assistant overlooks – and the more often it “fixes” things that weren’t broken.

The problem isn’t the monolith itself. The problem is the missing separation of responsibilities inside the monolith.

This matters because the obvious reaction is wrong. An app made of one file doesn’t need microservices. It needs a modular monolith: one application, one deployment – but with clearly separated layers for API, business logic and data access inside.

SQLite: good database, wrong place

SQLite is an excellent database. For a prototype, an internal tool or a mostly read-heavy app on one server it is often the best choice: no installation, no connection, fast. With WAL mode enabled, reads and writes coexist well.

SQLite becomes a problem once the app is meant to grow:

  • The database is a file on one server. A second server for more capacity or redundancy can’t sensibly share it. Horizontal scaling is out.
  • Only one writer at a time. Many concurrent writes – for example users entering data while an import runs – queue up.
  • On containers and serverless platforms, data loss looms. If the file lives in the container’s filesystem instead of a persistent volume, it’s gone with the next deployment. We see this surprisingly often.
  • Schema changes happen “on the side”. Usually the application code alters tables on start-up. At some point nobody knows which database is on which version.

So the honest answer to “Do I have to move off SQLite?” is: not immediately, but before you have more than one server, many concurrent writers or paying business customers with uptime expectations. The target is usually PostgreSQL – with versioned migrations, so every schema change is traceable and repeatable.

The invisible blocker: state in memory

A pattern that rarely gets noticed until it’s too late: logged-in sessions, login lockouts and rate-limit counters live in plain variables in the server process’s memory.

That has three consequences:

  1. Every restart or deploy logs out all users.
  2. Under attack these structures grow without limit until memory runs out.
  3. A second process – the simplest way to handle more load – would have its own, contradictory sessions. The app can’t even be doubled on the same server.

The fix is unspectacular: sessions and counters belong in the database or a central store such as Redis.

The missing data model: users yes, companies no

Many AI-built apps only know individual logins. For a product meant to be sold to businesses, the most important layer is missing: the organisation. It ties together:

  • several users of one company,
  • roles such as “company admin”,
  • licences or seats,
  • the contract status (trial, active, cancelled, suspended).

This is often the real scaling limit. The question isn’t whether the server can handle 500 accounts – it usually can. The question is whether the data model can represent 500 paying companies. Without an organisation layer the answer is no, however powerful the server.

How much can the app really take?

“We have 300 accounts, is that enough?” can’t be answered from the number of accounts. What matters is:

  • the number of concurrently active users,
  • the run time of the heaviest queries,
  • the load from background jobs such as imports.

You measure that with a load test that mimics real usage. Only then is it worth deciding on more servers, caching or a different database. Investing earlier often means expensive infrastructure for a problem you don’t have.

The way out: modularise, don’t rebuild

The temptation to throw everything away and rebuild it “properly” is strong. We almost always advise against it. An AI-built prototype usually contains valuable domain knowledge – edge cases, rules, data logic – that a rebuild would recreate from memory and lose along the way.

This order has proven itself:

  1. Pin down critical logic with tests. Before anything is restructured, automated tests describe what the app does today. They are the safety net for everything that follows.
  2. Split the backend into layers. API, business logic and data access are extracted from the big file step by step. The database stays as it is for now.
  3. Move state out. Sessions and rate limits go to the database or a central store.
  4. Migrate to PostgreSQL, with versioned migrations. Because the logic is now separated, little code changes.
  5. Break the frontend into components that fetch their data through the new API – view by view, while the old version keeps running.
  6. Measure, then scale. Monitoring and load tests show where the real limits are.

In parallel: monitoring with alerting, backups outside the production server and a restore that has actually been tested once. These three cost little and prevent an outage from being discovered by a customer.

Self-check: how scalable is your app?

Answer honestly. Every “no” is an item for the roadmap.

  • Can I change a feature without touching files that have nothing to do with it?
  • Are there automated tests for the most important business logic?
  • Does the database live on persistent storage – and survive a new deployment?
  • Are schema changes made through versioned migrations?
  • Do users stay logged in after a restart?
  • Could I start a second server process tomorrow without breaking anything?
  • Does my app know companies with several users, roles and a contract status?
  • Do I notice an outage before a customer does?
  • Is there a backup outside the production server, and have I ever restored it?

More than three “no”s for a product that’s supposed to make money soon? Then an outside view is worth it – before your first customers run the stress test for you.