My team and I recently had an unsettling experience with AI-assisted development.
Across several codebases, coding agents had helped us build, migrate, and transform software at remarkable speed. Then we began decoupling a packaged Next.js application, and everything changed.
Progress became strangely difficult.
Database connections we had removed would reappear. Direct calls to external services would grow back. Code redirected through our unified API gateway would later bypass it.
The AI understood, at least in conversation, that the gateway was supposed to own those integrations. Yet when completing a feature or fixing a broken flow, it repeatedly behaved as though the middle layer did not exist.
For a while, we wondered whether we were losing our minds.
Other projects had moved quickly. Why did this one fight every attempt to stretch it into a different shape?
The answer was not simply bad code or a weak AI agent.
Some codebases are genuinely harder for AI to transform because their architecture is encoded through hundreds of mutually reinforcing signals.
The boundary we planned to restore later
Like many prototypes, the project was initially optimized for visible progress.
The intended long-term flow was simple:
Presentation application
↓
API gateway
↓
Backend services
↓
Storage and external providers
During the prototype stage, some of those handshakes were shortened.
The Next.js application could reach the database and certain external services directly. The assumption was that these temporary connections could later be redirected through the proper backend.
At the time, this felt like a practical shortcut rather than an architectural decision.
The backend was still planned. The gateway still existed conceptually. We believed we were only removing some early friction.
That assumption turned out to be incomplete.
A protocol boundary does more than decide where a request is sent. It establishes where responsibility changes hands.
It helps define:
When that handshake is skipped, these decisions do not disappear. They are simply made implicitly inside the application closest to the user interface.
Over time, the prototype absorbs them.
Its data types begin to reflect the database. Its user flows assume particular storage behaviour. Its cache becomes tied to writes. Its server-side functions start coordinating outside services. Its deployment collects more credentials.
By the time the team is ready to “replace the backend,” there may no longer be one backend connection to replace.
Backend responsibilities have spread throughout the application.
A useful principle emerged from our experience:
Protocol friction is often ownership made visible.
The early handshake may feel expensive because its cost appears immediately. The cost of omitting it is harder to see because it accumulates gradually.
AI does not see your instruction in isolation
A coding agent does not look only at the latest prompt.
It also looks at the repository’s structure, dependencies, names, types, tests, configuration, and familiar framework patterns.
A Next.js application may contain signals such as:
A human instruction might say:
This application no longer owns persistence or external integrations. Use the gateway.
But the repository may still say, in hundreds of smaller ways:
This is a self-contained full-stack application with resources attached to it.
The AI follows the combined evidence.
A removed database call can look unfinished. A stub can look like a missing implementation. A deleted vendor client can look like a defect.
When the agent tries to make the application feel complete again, it restores the architecture it recognizes.
This is architectural gravity.
Some architectures exert more gravity than others
Frameworks do more than provide tools. They encourage a particular shape for the application.
Next.js makes it easy to combine:
When packaged for Vercel, that model becomes especially coherent:
Vercel application
├── renders the interface
├── interprets user actions
├── applies rules
├── manages identity
├── reads and writes data
├── calls outside services
└── controls its own cache
Attached resources
├── database
├── authentication provider
├── payment provider
├── object storage
└── external APIs
In that model, the application is the authority.
The database is treated as storage. External APIs are treated as remote tools. The application still decides what each operation means.
Our intended architecture was different:
Next.js presentation application
↓
Unified API authority
├── owns business operations
├── controls persistence
├── coordinates services
├── protects credentials
└── returns authoritative results
The first is a resources-attached-to-the-app model.
The second is a capabilities-behind-an-authority model.
These are not simply two ways of connecting the same pieces. They place responsibility in different locations.
Vercel suppresses the usual warning signs
In a more visibly separated system, adding business logic to the presentation project creates friction.
You need an API contract. You need another service. You need to think about credentials, networking, ownership, and error handling.
Those costs force the team to make architectural decisions early.
A packaged Next.js application on Vercel hides much of that friction.
A developer can place a mutation in a server-side function, connect it to a database, deploy it, and quickly obtain a working result.
That convenience is real.
It also makes coupling appear cheaper than it is.
The cost becomes visible later, when someone tries to separate the application from its database, identity system, cache lifecycle, or external providers.
The architecture was not avoided. It was simply made less visible.
The backend becomes storage instead of an authority
One of the clearest problems was that the application did not treat the backend as the owner of a capability.
It treated the backend as a low-level storage or transport layer.
In a service-oriented system, the application might say:
Submit this application.
The backend then decides what rules apply, which services must be called, how the result is stored, and what success means.
In the coupled model, the Next.js application effectively says:
Read these records.
Check these conditions.
Call this provider.
Write these fields.
Update this status.
Return the result.
Even when those calls pass through an API, the application may still own the workflow.
The backend is being used as a remote device rather than an authority.
“But all our database queries are in one file”
This is a common and reasonable objection.
A team may say:
All database queries are already centralized.
Or:
All external calls go through the `/api` routes.
Those are good organizational choices. They can improve security and reduce duplication.
But they do not automatically create decoupling.
Centralization tells you where the code lives.
Decoupling tells you where authority lives.
One database file can still expose storage-shaped operations while the rest of the application decides what to read, what to write, and in what order.
Likewise, moving an external call into `/api` may hide credentials from the browser, but Next.js may still own the provider choice, workflow, retry behaviour, and interpretation of the result.
The call has been proxied. The responsibility has not necessarily moved.
A useful test is:
Could the backend change its storage system and service providers without forcing the presentation application to understand the change?
When the answer is no, the code may be centralized and well organized, but it is not yet truly decoupled.
The database may be part of the assumed environment
We initially treated Neon PostgreSQL as an implementation detail.
The application did not.
Its storage model influenced:
* identity;
* object shapes;
* relationships;
* UI state;
* mutation order;
* read-after-write behaviour;
* caching;
* errors;
* generated types.
The database was not merely holding state. Its speed and behaviour had become part of the application.
This became obvious when one flow was moved to blob storage.
The original sequence looked simple:
write
read the latest value
render
With a fast random-access database, this often behaves like an ordinary local sequence.
The database quietly handles coordination and immediate visibility.
With blob storage, the operation may instead be:
read the object
change it
replace it
read it again
We encountered a race condition and had to serialize the write-before-read flow.
That was not unnecessary complexity introduced by the new design.
It was complexity that the old database had concealed.
The application did not depend merely on “storage.” It depended on a set of hidden guarantees:
fast random access
+ mutable records
+ immediate visibility
+ safe concurrent writes
Changing the storage exposed the real contract.
Next.js can hide that storage contract
Code such as this appears neutral:
await saveState(nextState)
const currentState = await loadState()
But it says nothing about what `saveState()` actually guarantees.
Was the write immediate? Was it queued? Was a whole object replaced? Will another request see it right away?
Next.js can make the sequence look even more certain:
await saveState(nextState)
revalidatePath('/account')
redirect('/account')
Visually, it suggests that writing, cache invalidation, and rendering are one ordered process.
They may not be.
Refreshing the framework’s cache does not guarantee that an external storage system has reached the state the next render expects.
Once another backend becomes authoritative, the UI may need to handle pending operations, delayed visibility, version conflicts, or changes made elsewhere.
The original database made those concerns easy to ignore.
The ORM may be doing much more than database access
A bundled ORM often becomes part of the application’s architecture.
It may provide:
Removing the ORM therefore leaves more than an empty database adapter.
It creates apparent gaps throughout the project.
The coding agent sees those gaps and discovers an easy way to fill them: restore the ORM.
This becomes especially strong when the same object is used as a form model, API payload, database record, server response, and UI model.
That feels efficient during prototyping.
Later, it reveals that the layers were never truly separated.
Permissive typing can make this worse. When gateway responses, database records, vendor payloads, and UI models are all treated as any, the compiler can no longer expose where one architecture ends and another begins. Old assumptions continue to compile after calls are redirected, and the AI is free to use whichever data shape makes the immediate feature work.
Strict contract types do more than prevent ordinary bugs. They create visible boundaries for both developers and coding agents. A type mismatch during migration is often not an inconvenience to suppress - it is evidence that an old architectural assumption is still leaking through.
Why some codebases are harder for AI to stretch
This experience changed how I think about AI-assisted estimates.
Feature complexity is not the only factor.
Some repositories are highly malleable. Their boundaries are clear, their dependencies are narrow, and the requested change fits the way the system is already divided.
Other repositories are architecturally dense.
Their framework, database, deployment model, cache behaviour, types, and tests all reinforce one complete application pattern.
In those projects, the AI is not merely editing code.
It is being asked to resist the repository’s strongest pattern.
The more idiomatic and self-contained the original application is, the more likely the agent is to interpret a new architecture as something incomplete that needs to be repaired.
That does not mean the framework is bad.
It means its productivity model has a shape.
Changing that shape later may cost much more than expected.
How to counter architectural gravity
Prompting helps, but prompting alone is not enough.
The new architecture must become more convincing than the old one.
Remove the old paths
Do not merely tell the agent not to use the ORM or vendor SDKs.
Remove them.
Delete obsolete database settings, generated persistence types, old schema files, and provider credentials from the presentation project.
A forbidden implementation that remains easy to access will eventually return.
Give the gateway meaningful operations
The gateway should expose business intent rather than database commands.
Prefer:
submit application
resolve current identity
save draft
approve request
complete verification
Avoid interfaces that merely reproduce database operations over HTTP.
The gateway should hide how the operation is implemented.
Separate the types
Do not let one object represent the database row, API contract, domain entity, and UI model at the same time.
Some mapping code may feel repetitive, but it creates a real boundary.
Enforce the rules
Use package boundaries, lint rules, tests, and automated checks to prevent:
The architecture should fail visibly when it is violated.
Review AI changes for regressions
Do not check only whether the feature works.
Also check whether the agent:
A passing test does not prove that the boundary survived.
Establish the tiered flow early
The most important lesson is not limited to Next.js or Vercel.
It is about when architecture becomes expensive.
A protocol boundary does not need to be elaborate during prototyping. It can begin with a narrow gateway and a few clear operations.
But the direction of authority should be established early.
When an independent backend is part of the intended design, new features should grow through that boundary rather than around it.
Otherwise, “we will replace the backend later” becomes a misleading estimate.
The later work is not simply replacing a backend connection.
It is extracting identity, workflow, storage assumptions, service coordination, credentials, types, and business authority from an application that has gradually absorbed all of them.
That is not a dependency swap.
It is architectural decomposition.
Budget for architecture resistance
AI-assisted planning should account for this resistance.
A self-contained vertical application may be extremely fast to prototype. It may also be expensive to divide later because every feature reinforces the original ownership model.
Before shortening the protocol flow, teams should ask:
Is this application intended to remain the permanent authority for everything behind this boundary?
When the answer is yes, direct integration may be entirely appropriate.
When the answer is no, the handshake is not merely overhead.
It is the cheapest opportunity to define where one system ends and another begins.
You are probably not losing your mind
When an AI coding agent transforms one codebase effortlessly and fights another at every step, the difference may not be your prompting or the agent’s intelligence.
The repository may be expressing a stronger opinion than you are.
AI accelerates patterns already encoded in software. It does not automatically distinguish deliberate architecture from prototype convenience.
When a framework, deployment platform, ORM, database, cache model, and type system all reinforce the same design, the agent may interpret deviation as damage.
Some codebases are therefore genuinely harder for AI to stretch.
The solution is not simply to repeat the prompt more forcefully.
You must remove the old architecture’s signals, expose the hidden storage contracts, establish a real authority boundary, and make the desired design the only implementation that looks complete.