The Difference Between a Demo and a Durable Product
Why impressive AI-generated demos still need hardening before they can survive users, edge cases, deployment, and change.
A good demo proves that an idea can exist. It shows the happy path, makes the workflow tangible, and gives the team something real enough to react to.
A durable product has to prove something harder: that the important paths stay safe when confused users, partial data, retries, failed deploys, and the next dozen features show up.
We reviewed an app recently where the gap was obvious once you knew where to look. The dashboard relied on an identity-provider email header to decide which data a visitor could see — a reasonable pattern for a protected portal. But the fallback rule treated a missing header as internal full access. Fine for localhost. Not fine if the route becomes reachable through a misconfigured proxy, a preview environment, or a copied deployment script that skips the CDN.
That same codebase had an endpoint that accepted arbitrary SQL behind a bearer token, a deploy script that printed generated secrets to the terminal and told the operator which ports to open, and a cron installer that wrote a long production wrapper directly onto the server. Each shortcut kept the prototype moving. Together, they painted a clear picture: this product had crossed into operational software without updating its standard of proof.
Demos Optimize for Momentum
A demo gets to be narrow. It can assume the user is friendly, the database has clean records, and whoever runs the app will know how to recover when something breaks. A lot of AI-built products start here because speed is the whole point — a founder wants to see the workflow, a team wants to validate the market, or someone wants to prove that a manual process can be automated.
Nothing wrong with that. It just means the contract is different.
A demo asks "can we see this work?" A product asks "can we trust this when
nobody is watching?" The moment the app stores customer data, handles money,
triggers external actions, or becomes part of someone's daily operations, the
bar shifts. Authentication needs to be enforced on every protected route.
Authorization needs to follow the data, not just the UI. Jobs and payments need
to survive retries. Errors need to tell operators what went wrong. And the
observability story has to be something better than console.log and hope.
Here is the practical difference:
| Area | In a demo | In a durable product |
|---|---|---|
| Authentication | The known test user reaches the screen through the expected login flow | Every protected route, API handler, server action, and webhook rejects missing or invalid identity |
| Authorization | The UI hides records the demo user should not see | Tenant, role, and ownership checks run close to the data and are tested with cross-user requests |
| Jobs | A cron, import, or payment callback succeeds once while someone watches | Work is idempotent, checkpointed, retry-safe, and explicit about failed or duplicate items |
| Observability | Console output explains the run while the builder is present | Requests, jobs, external calls, and user-visible failures share correlation IDs and structured logs |
| Secrets | Local env files and deployment notes are enough for one operator | Secrets live in managed storage, avoid terminal output, rotate predictably, and have named owners |
The Boundary Is Usually Auth
The first production-readiness question is straightforward: what does this request actually prove about the caller's identity?
Framework guides like the Next.js authentication guide and provider docs like Clerk's App Router auth docs make the same practical point — protected server-side work needs explicit checks, not hopeful routing. Redirecting an unauthenticated user away from a page isn't enough if the API route, server action, or database query behind it can still be called directly.
In the app we mentioned, the risky part wasn't using a header for identity. It was treating the absence of that header as proof of internal access:
// Fragile: a missing header silently becomes full internal access.
const email = req.headers['x-auth-email'] ?? INTERNAL_ADMIN;
// Sturdier: absence means unauthenticated, full stop.
const email = req.headers['x-auth-email'];
if (!email) {
return unauthorized();
}In production, absence should mean "unauthenticated" unless a documented, monitored network control says otherwise.
The second question is harder to get right: what is this identity allowed to touch?
The OWASP Authorization Testing Automation Cheat Sheet pushes teams toward systematic role-and-resource testing, which matters more than usual in AI-built apps. Agents tend to add routes by copying the nearest working handler. If that handler filters data in the UI but not in the query, the product can look correct in the browser while leaking data through a direct API call, an export endpoint, a background job, or an admin tool.
Good authorization is repetitive and unglamorous. It names which actor can do which thing to which resource, and enforces that close to the data. A customer can't see another customer's records because the query or policy refuses it — not because a dropdown hid the option. (Both halves of this failure — leaked secrets and interface-only authorization — get a deeper treatment in The Secrets Your AI Already Leaked.)
Flexible Interfaces Become Dangerous Interfaces
An arbitrary SQL proxy is a classic prototype move. It's fast to build, powerful, and easy to drive from an agent or admin script. The problem is that it creates an API surface nobody can reason about. The endpoint isn't "approve invoice" or "retry import" — it's "run whatever query the caller sends." Even with bearer auth and a scoped database role, a reviewer has to imagine every possible query to assess the risk.
Production systems earn their keep with narrow commands: typed inputs, validated at the boundary, backed by least-privilege database access, with an audit trail and errors that don't leak internals. That doesn't mean ripping out every admin escape hatch. It means making escape hatches rare, owned, logged, and visually distinct from the product API.
Infrastructure follows the same logic. A one-shot deploy script is a fine way to bootstrap. It stops being fine when generated tokens end up in a terminal transcript, environment files are assembled by string interpolation, and the runbook tells someone to open broad network access after the fact. Production infrastructure has to be repeatable, version-controlled, and recoverable — with secrets in a managed store, ingress scoped intentionally, and the whole setup reproducible from source control.
Security headers belong in the same category. They're not a polish pass you do before launch. The OWASP HTTP Security Response Headers Cheat Sheet is a practical baseline for Content Security Policy, HSTS, Referrer-Policy, and clickjacking protections. A production app needs to know where those headers are set and verify they survive the next framework upgrade or hosting migration.
Jobs and Payments Need Replay Discipline
Retries are where demos quietly fall apart. During a demo, a human clicks a button once. In production, the system receives duplicate webhooks, times out after partially completing work, restarts a cron job mid-run, or processes a payment callback twice because the first response was slow.
The cron wrapper in the app we reviewed was doing several jobs in a single shell pipeline: syncing a repo, running a scan, invoking an agent, consolidating records, checking database state, and notifying a human. It even swallowed some failures so the pipeline could keep going. That's understandable while a team is still learning the shape of the workflow. It's not enough once the output drives customer-facing data or business decisions, because nobody can tell which step failed or whether it's safe to re-run the whole chain.
Well-built jobs have explicit failure semantics. Each step knows whether it can be retried, whether a duplicate should be skipped, and where a failed item goes to wait for review. Payment providers document this pattern well — Stripe's idempotent request docs explain how an idempotency key makes retrying the same charge safe. The same discipline applies to import jobs, notification dispatches, scheduled tasks, and anything an agent kicks off.
Observability Is Part of the Product
Console logs work while you're building. They stop working when a request touches a browser, a server route, a database, a queue, a third-party API, and a background worker — and the thing you need to debug happened three hours ago in a flow you didn't write.
The question operators actually need to answer is specific: which user was affected, what release was running, what external call failed, and whether the problem is still happening. Without request IDs, structured logs, and at least basic tracing, that investigation turns into grepping unstructured output across multiple services and hoping the timestamps line up.
OpenTelemetry's observability primer frames this around three signal types: traces, metrics, and logs. You don't have to adopt the full platform on day one. The useful starting point is smaller: attach a request or correlation ID to the critical path, carry it through background work, and set up alerts on the failures that affect users, money, or data integrity.
A Demo-to-Durable Checklist
Before calling an AI-built app production-ready, walk through the boring paths:
- Protected routes, API handlers, server actions, and webhooks reject missing or invalid identity.
- Authorization is enforced near the data and tested across roles, tenants, and resource ownership.
- Public endpoints have rate limits, narrow CORS, security headers, and abuse-control measures proportional to the threat model.
- Secrets live in managed stores, rotate on a schedule, and never show up in deployment output.
- Payments, imports, notifications, and scheduled jobs are idempotent and retry-safe.
- Background jobs are versioned, observable, and owned by a specific team or person.
- User-facing errors are intentional. Operator-facing errors carry enough context to act on.
- Privacy, retention, deletion, and audit requirements are implemented in code, not just described in a policy document.
How the Diagnostic Looks at This
In a Post Code Labs diagnostic, this article maps most directly to production hardening, security baseline, observability, compliance, and release pipeline evidence — checkpoints that belong to the full Post Code Labs consultation, which scores them across its twelve categories. Scoring is concrete — pass, partial, or gap — with file references and a short explanation of the risk.
The output isn't "rewrite everything." More often, the right plan is targeted: turn a header assumption into explicit auth middleware, replace a generic SQL bridge with three typed admin commands, move hardcoded secrets into managed configuration, and split a monolithic cron wrapper into individual observable jobs with replay guards. The goal is to keep the product knowledge the demo uncovered while raising the operating standard around it.
What to Do Next Monday
Pick one workflow that would damage trust if it failed — login, checkout, data export, customer dashboard, invite flow, or a scheduled job. Trace the request from the browser or webhook to the database and back. Write down every place where the code assumes identity, tenant boundaries, permissions, data freshness, secrets, network access, or successful retry without actually verifying it.
Then make one concrete improvement, sized to fit in a single sprint:
- Auth: centralize the protected-route check and add tests for missing, expired, and malformed identity on the routes that read or mutate customer data.
- Authorization: move one role or tenant rule out of the UI and into the query, policy, or service method that fetches the records.
- Jobs: add an idempotency key, status table, or replay guard to the job, webhook, import, or notification path most likely to be retried.
- Observability: carry one request ID through the route, background work, database write, and log entry for a workflow that support will eventually need to debug.
- Secrets: remove one generated token or private value from deployment output, put it in the managed secret store, and write down its owner and rotation path.
That's the real gap between a demo and a durable product. The demo showed the idea could work. The product has to keep proving it, every day, without anyone watching.
References
Next.js authentication guide
[nextjs.org]Clerk: App Router auth reference
[clerk.com]OWASP Authorization Testing Automation Cheat Sheet
[cheatsheetseries.owasp.org]OWASP HTTP Security Response Headers Cheat Sheet
[cheatsheetseries.owasp.org]Stripe: idempotent requests
[docs.stripe.com]OpenTelemetry observability primer
[opentelemetry.io]