App Works in Preview but Not in Production: 404 on Refresh, No Data, Login Sent to localhost

A deep link gives a 404, the data never arrives, and sign-in sends people to localhost. None of it is a bug in your code, and your server will tell you everything is fine.

The same build deployed twice: with the settings it had in preview a typed URL returns 404, no data arrives and sign-in lands nowhere, while the API's own log records a clean 200 for the request the browser discarded, and with four settings changed everything works

It works in preview and breaks in production, and the gap between those two sentences is where a lot of projects stall. Not because anything is hard, but because nothing is wrong.

The code is the same code. The build is the same build. What changed is a handful of settings that had no effect at all on the machine you wrote it on, because on that machine every answer happened to be right.

So we took one build, deployed it twice, and had a real browser tell us what a person sees — while reading the API’s own log alongside.

Watch it instead

Getting online is number 7 in our countdown of what actually goes wrong for people building with AI.

Video: We Analyzed 130,000 Vibe Coder Posts. These Are the Problems That Actually Hurt.
Watch it instead We Analyzed 130,000 Vibe Coder Posts. These Are the Problems That Actually Hurt.
From the Back4app channel. This guide is the long version of its seventh position; the countdown itself is in the companion post. Open on YouTube →

What actually differs between the two deployments?

Nothing in the code, and everything in what a person sees:

The same build deployed twice: with the preview settings a typed URL returns 404, no orders load and sign-in lands on an error page, while the API logs a clean 200 for the blocked request, and with four settings changed every check passes

Four separate failures, four separate causes, and one thing in common. Every one of them happens between components that are each behaving correctly.

Why did the API say 200?

Because it was 200. This is the line we kept rereading:

what the api says about that request   200 /api/orders origin=…:7502 cors=NOT SENT

The request arrived. It was answered. The response body contained all three orders. The browser then discarded it, because the reply did not name the origin that had asked for it.

Your server log, your uptime check and your error tracker all show a healthy backend, and the page shows nothing. Nobody is lying.

This is also why the problem survives debugging. Any tool without that rule — curl, Postman, a script on your laptop — sees a perfectly working API, because the rule belongs to the browser.

Why does a typed URL 404?

Because the server went looking for a file. Your app knows what /orders means, but it has to be running first, and it is not running until some HTML arrives.

Click the link inside the app and the browser never asks the server — the app handles it. Type the same URL and the server gets asked for orders, finds no such file, and says so:

/orders typed into the address bar     HTTP 404

After telling the host to serve index.html for unknown paths, that identical URL returned 200 and the app read the path itself. Nothing else changed.

This is also why the failure is easy to miss. Nobody types URLs while building — they click. The first person to hit it is a user following a shared link, which is the worst time to find out.

Where did sign-in go?

To a machine that is not yours:

where "sign in" lands   chrome-error://chromewebdata/ — nothing there

The redirect was set once, while developing, to the dev server’s address, and nothing updates it on deploy. Sign-in is also the single hardest flow to test by accident, because you are already signed in on the machine where you built it.

The same build, two deployments
0 of 3orders shown with the preview settings
→
3 of 3after four settings changed
200logged by the API for the request the user never received
Read out of the live DOM in real Chrome, alongside the API's own request log. Identical across two runs.

The one a setting cannot fix

A variable added after the build is not in the bundle, and no amount of setting it afterwards will put it there.

Front-end builds inline what they can see at the moment they run. We built once without the sign-in URL and the value shipped as an empty string — the dashboard can say anything you like after that, and the file on disk still says nothing.

the bundle shipped with an empty sign-in link, because the variable
did not exist when the bundle was built.

So the rule is simple and worth internalising: changing a variable the front end reads means rebuilding. Changing one the server reads usually just means restarting.

Why does none of your monitoring see this?

Because nothing it watches is broken. The page returned 200. The API returned 200. The container was healthy. The error tracker has nothing to report, because no error was thrown anywhere — the browser declining to hand a response to your code is not an exception in the usual sense.

That is the same shape we measured in your app is down and nothing told you: a monitor answers its own narrow question correctly, and nobody asked whether a person could use the thing.

What should you do first?

Four checks, each a few seconds, on the deployed site rather than the preview:

  1. Type a deep URL straight into the address bar and press enter.

  2. Open the network tab and look for a request that succeeded but was blocked.

  3. Click sign in and read where you land.

  4. Search your built bundle — not your config — for a value you expect to be there.

Those four cover everything in this article. Do them once per deploy and the gap between “it worked in preview” and “it works” stops being a mystery.

The lab is in the repo — one build, two deployments, a real browser, and the API’s own log read alongside.

Tagsdeploymentcorsenvironment-variablesspaoauthvibe-coding

Frequently asked questions

Why does my app 404 when I refresh the page?

Because the server looked for a file with that name and there isn't one. Your app handles that path in the browser, which only works once the app is already running. We measured it: a URL typed into the address bar returned HTTP 404 until the host was told to serve index.html for unknown paths, after which the same URL returned 200 and rendered.

Why does my API work in Postman but not in the browser?

Because the restriction is enforced by the browser, not the server. In our run the API returned 200 and the request was answered correctly — the browser then discarded the response because it did not name the origin that asked. Any tool without that rule, including curl and Postman, sees a perfectly healthy API.

Why does my login redirect to localhost in production?

Because the redirect URL was set once, during development, and nothing updates it on deploy. We clicked sign-in on the deployed app and landed on a browser error page, because the address it was sent to only exists on a developer's machine.

Why is my environment variable undefined in production?

If it is read in front-end code, it was inlined when the bundle was built. The build writes in whatever it can see at that moment, so a variable added afterwards is not in the file. Setting it in the dashboard changes nothing until the app is built again.

Why does my uptime check say everything is fine when the site is broken?

Because nothing it watches has broken. In our measurement the API logged a clean 200 for the very request the user never received, the page itself returned 200, and the container was healthy throughout. All four of these failures happen between correct components.

What should I check before calling a deploy done?

Four things, each taking seconds: type a deep URL straight into the address bar, look in the network tab for a request that succeeded but was blocked, click sign-in and read where you land, and search your built bundle — not your config — for the value you expect to find.

Further reading

Reviewed & verified by Back4app Engineering, Editorial review at Back4app

Tested Oct 27, 2026 — Local: Node 22.21.1 on macOS, arm64 · Google Chrome driven over the DevTools Protocol · One static host, one API, no dependencies · Back4app, free plan.

Changelog

  • Nov 3, 2026 — The companion repository is not published yet, so the link to it and the button were removed; they come back when it is.
  • Oct 28, 2026 — The companion repository is published, so the button and the inline link to it are back.

More from Back4app Engineering

Containers · error-guide

Why Your App Works Locally and Crashes When You Deploy It [Full Repo]

Backend · report

Why Vibe-Coded Apps Break: 13 Problems From 130,000 Posts, Ranked, With the Fix for Each

Containers · tutorial

How to Host a Static Website on Back4app Containers With Docker and nginx [Full Repo]