Engineering
What Usually Breaks Between Prototype and Production
The practical gaps that show up once a prototype meets accounts, permissions, billing, failures, migrations, and the rest of a real operation.

A prototype answers a narrow question. Can this flow exist. Does the shape of the product make sense when someone clicks through it. That is a useful answer, and it is not a production system. The break between the two is rarely a scaling problem. It is a set of responsibilities the prototype was allowed to skip.
Naming them early does not mean building all of them on day one. It means refusing to treat the skipped parts as polish. They are the parts that decide whether the software can be operated, trusted, and changed.
Authentication is more than a login form
A prototype often has one shared user, or a login that creates a session and stops there. Production needs an account model. How a person signs up or is invited. How a password is reset, or how a single sign-on provider is connected. What happens when a session expires in the middle of a form. How an account is disabled without deleting the history attached to it.
The failures here are mundane and serious. A reset link that never expires. A session that stays valid after access should have ended. An invite flow that creates a second account because the email did not match exactly. None of these require a clever architecture. They require the account lifecycle to be a real feature, with states, instead of a gate in front of the demo.
Authorization is not authentication
Signing in proves who someone is. It does not decide what they may do. Prototypes collapse the two, because everyone using the demo is allowed to do everything. Production separates them the moment there is more than one kind of person.
List the actions, not the pages. Creating a record, seeing someone else’s queue, changing a price, exporting data, inviting a teammate, changing billing, impersonating a user for support. Each action needs a check at the point where it happens, on the server that performs it. Hiding a button is a courtesy. It is not the control.
Permissions also rot if they are copied into every query by hand. One missed check becomes a link that should not have opened. Decide where the rule lives, test the forbidden cases, and treat a bypass as a defect rather than a follow-up.
Tenant isolation has to be structural
If the product serves more than one organisation, the prototype’s single pool of data becomes a liability the day the second customer arrives. Filtering by an account id in most screens is not isolation. Isolation means a request cannot read or write another tenant’s data even when the filter is forgotten, a report is added later, or a background job runs without a user in front of it.
That includes files, search indexes, caches, logs, and emails. A download URL that is guessable, a log line that contains another customer’s payload, a support tool that queries across tenants without an explicit audited reason: each one undoes the filter on the main screen. The boundary has to be boring and consistent. If an engineer cannot say where the tenant is applied, the prototype has not crossed into production.
Billing is an operational system
Taking a payment in a prototype can be a single successful charge. Operating billing is a set of states. Trial, active, past due, cancelled. What the product may do in each state. What happens when a card fails. How a plan change is prorated, or deliberately not. How a person gets a receipt. How support can see why access changed without reading a payment provider’s dashboard as the source of truth.
The common break is a boolean called isPaid, set to true when a webhook arrives and never revisited. Webhooks are missed, duplicated, and delayed. Access decisions need an explicit subscription state that can be reconciled, not a flag that was true the last time something worked. And the product needs a defined behaviour for the awkward middle: payment failed, but the team is mid-task. Cutting access with no warning and no explanation is a product incident, not a billing detail.
Errors need a designed response
Prototypes show the happy response. Production spends its life in partial failure. A dependency times out. A validation fails on one field. A job stops halfway through a batch. The user refreshes and submits again.
Each of those needs a behaviour someone chose. What the person sees. Whether the action is safe to retry. Whether the system already applied the change. A vague “something went wrong” with no way to tell attempt from success produces duplicate records and support tickets that cannot be diagnosed. An error that leaks an internal stack into the interface is the opposite problem: too much information, in the wrong place.
Write the important failures down as cases. Not every exception class. The ones a person can hit, and the ones an operator must notice.
Observability is how you run it
If the only way to know the system is healthy is to click through it, the prototype is still a demo. Production needs a small set of signals tied to real outcomes. Did sign-in succeed. Did the job finish. Did the payment state change. Are requests failing for one tenant or for everyone.
Logs should be searchable by the identifiers an operator will actually have: an account, a request, a user, a job. They should not be the only copy of business history, and they should not contain secrets or another tenant’s content for convenience. Alerts should point at something a person can act on. A page that is red because a threshold was copied from a tutorial will be ignored by the third week. Enough signal to answer whether the system is working, and which account is affected, is the bar for leaving the prototype.
Migrations and change
A prototype can drop its database and start again. A production system has yesterday’s data, and someone is using it while you ship. Schema changes, backfills, and feature rollouts are part of the engineering, not a later phase called maintenance.
The breaks are predictable. A migration that locks a busy table. A new required field with no value for old rows. A code deploy that expects the new column before the migration has run. A backfill that cannot be resumed after it fails halfway. Treat changes as operations with an order, a way to tell they finished, and a way to ship the code without demanding a perfect instant cutover.
Testing the paths that matter
A prototype is tested by using it. That remains valuable, and it does not cover the cases production will hit. The tests that earn their keep are the ones around rules: permissions denied, tenant boundaries, status transitions that should be rejected, billing states, idempotent retries, and migrations against a copy of real-shaped data.
Aim the automated tests at those rules. Use a smaller number of end-to-end checks for the critical journeys, including at least one path that is supposed to fail cleanly.
Operational edge cases
A few situations show up in almost every product once real people arrive. Two requests update the same record. A person double-clicks submit. An invite is accepted after it was revoked. A file is larger than expected. A clock crosses a day boundary and a trial ends during a session. A background job runs twice. An administrator needs to repair a record without pretending to be the customer in a way that leaves no trace.
None of these require a new platform. They require a chosen behaviour: reject, merge, retry safely, or record an explicit override. The software is ready to leave the prototype stage when those behaviours exist for the actions that cannot be wrong, and when someone can see that they held.


