When AI-generated HTML needs a backend A developer published a checklist for deciding when an AI-generated HTML page can be statically hosted and when it needs a real backend, warning that polished demos often hide missing infrastructure. The checklist covers localStorage being per-browser rather than a shared database, localhost and relative API URLs breaking once a page is deployed, and credentials embedded in client-side JavaScript being visible to every visitor, with the recommendation to keep privileged API calls behind a server-side endpoint that validates requests and enforces rate limits. AI disclosure: I used AI assistance to draft and edit this article. I reviewed the technical claims against the browser documentation linked below and checked the examples for consistency. The code is intentionally simplified. An AI assistant can produce a polished HTML demo in a minute. The page has buttons, calculations, a form, and maybe a chart. It looks like an application, so it is easy to assume that publishing the HTML also publishes everything behind it. That last step needs a closer look. A static page can render, calculate, filter, animate, and remember a preference in one browser. It cannot safely hold a private API key, turn every visitor's browser into your shared database, or make a service running only on your laptop available to everyone else. Here is the checklist I use to decide whether a generated page is ready for static hosting or needs a backend. This code can make a feedback form look finished: js const items = JSON.parse localStorage.getItem 'feedback' || ' ' ; items.push { message, createdAt: new Date .toISOString } ; localStorage.setItem 'feedback', JSON.stringify items ; render items ; It preserves the current visitor's entries in that browser. It does not send them to the page owner, and another visitor will not see them. Browser storage is useful for a draft, a preference, or a local-only tool. It is not a shared submissions database. There is another preview trap: the required behavior of localStorage for file: URLs is undefined and can vary between browsers. A result you see after double-clicking an HTML file is not a reliable test of what storage will do after deployment. If you only need a visual demonstration, keep the data in memory or use a small fixture and label the action clearly: "Demo only — nothing is submitted." If someone must receive the submission, use an HTTPS endpoint and show what happened after the server responds. localhost means Generated code often contains a call like this: js const response = await fetch 'http://localhost:3000/api/quote' ; const quote = await response.json ; While you develop, that address can reach a server on your machine. After you send the page to somebody else, the same code runs in their browser. localhost then means their device, not yours. A relative URL does not magically preserve your development backend either: fetch '/api/quote' ; On a static host, this normally asks that host for /api/quote . If no such endpoint exists, the page may receive a 404 or an HTML fallback instead of JSON. For a self-contained demo, replace the request with explicit sample data and keep the simulated behavior visible. For live data, deploy an endpoint your visitors can reach, then test its response and CORS policy from the public page. A private credential does not become safe because it sits inside a JavaScript variable or an HTML comment: // Simplified anti-example: never publish a real privileged credential here. const response = await fetch 'https://api.example.com/private-data', { headers: { Authorization: 'Bearer SERVER ONLY KEY' } } ; Every visitor can download the page and inspect its scripts. Obfuscation changes how the credential looks; it does not establish a secret boundary. Before publishing, inspect the actual export directory rather than only the project root. A bundler can copy a secret into its output even when .env itself is excluded. Check inline scripts, source maps, generated configuration, fixtures, and comments too. Remove private customer data at the same time. If a privileged API call is required, keep that credential on a server. The browser calls your limited endpoint; your endpoint validates the request and calls the upstream service: // Server-side route sketch. Add authentication, validation, and rate limits for a real service. export async function GET { const upstream = await fetch 'https://api.example.com/private-data', { headers: { Authorization: Bearer ${process.env.SERVICE API KEY} } } ; if upstream.ok { // Do not pass an upstream secret or sensitive response through by default. return Response.json { error: 'Upstream request failed' }, { status: 502 } ; } return Response.json await upstream.json ; } Some client-side API identifiers are deliberately public and can be restricted by origin or scope. That is a provider-specific design, so follow the provider's documentation. Do not treat a server credential as public just because the variable name contains KEY . A local file, a development server, and a public HTTPS page are different environments. JavaScript modules loaded from file: URLs can trigger CORS errors. Test modules through an HTTP server, and make sure the server returns JavaScript files with a JavaScript-compatible MIME type. An apparently missing module can be a delivery problem rather than an import mistake. For a public page that calls another origin, that server's CORS policy controls whether browser JavaScript may read the response. mode: 'no-cors' is not a general way to unlock JSON for your application. If you control the API, configure the origins it should serve; if you do not, a server-side integration may be the right boundary. Also check for http:// dependencies in an HTTPS page. Browsers restrict mixed content, and fetch requests to insecure resources can fail. Camera, microphone, service worker, and modern clipboard features have additional secure-context and permission requirements. http://localhost receives special treatment for development, which can hide a deployment problem. Feature detection is more useful than assuming the preview and the public page have the same capabilities: if window.isSecureContext && navigator.clipboard?.writeText { await navigator.clipboard.writeText text ; } else { // Show selectable text and a clear manual-copy instruction. showManualCopy text ; } A static page is a good fit when the result depends on the current visitor's input and public assets: a calculator, a proposal preview, a chart from bundled data, or a small editing tool whose output can be downloaded. Add a backend when the product needs shared state or authority: submissions that another person must read, accounts and permissions, shared edits, private upstream credentials, privileged actions, or durable data across visitors and devices. Before sharing the page, run this short review: The goal is not to add a backend to every generated page. It is to make the page honest about where computation, data, and authority live. Once those boundaries are clear, static hosting can stay simple without pretending to provide server behavior.