# 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.

URL: https://www.back4app.com/blog/works-in-preview-not-in-production
Published: 2026-11-03 · Tested: 2026-10-27
Tested on: 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
Publisher: Back4app Engineering

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.

## 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](/blog/blog-assets/it-worked-in-preview/it-worked-in-preview.png)

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:

```text
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.

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:

```text
/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:

```text
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 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.

```text
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](https://www.back4app.com/blog/health-check-passes-but-app-is-down): 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](https://github.com/templates-back4app/preview-lab) — one build, two deployments, a real browser, and the API's own log read alongside.
