Backend Development Engineering

Your Demo Worked Locally. The Staging Server Returns a Blank Screen to the Client.

Why local development environments consistently lie to you, how to systematically diagnose a blank staging screen, and the deployment awareness that stops this from happening before clients see it.

Meritshot8 min read
DeploymentDebuggingStagingFrontendCI/CDEnvironment Variables
Back to Blog

Your Demo Worked Locally. The Staging Server Returns a Blank Screen to the Client.

You spent two weeks building the feature. It looked perfect in your browser. You ran it one more time before the client call, everything loaded, the animations were smooth, the API calls returned in under 200ms.

Then you deployed to staging.

The client is on the call. They share their screen. White page. No error message. No loading indicator. Just silence.

This is not a beginner's mistake. It happens to developers with three years of experience, to teams with proper CI/CD pipelines, to agencies with established deployment checklists. The reason it keeps happening is not carelessness — it is a fundamental and common misunderstanding of what "works locally" actually means and what the differences between local and staging environments introduce.


Why "It Works on My Machine" Is an Architectural Statement

When a developer says "it worked locally," they are describing a system running under a very specific set of conditions: their exact Node.js version, their operating system, their file system case sensitivity, their locally installed packages, their .env file, their browser with their cached assets, and their development server with hot module replacement and relaxed CORS settings.

None of these things are necessarily true on staging.

The blank screen is the browser's way of telling you that one of those assumptions has been violated — but because everything failed silently, you don't know which one.

The most common culprits cluster into predictable categories:

  1. Environment variables missing or named differently on the server
  2. Build output differences between development and production modes
  3. Asset paths that work relative to a local dev server but break on a subdirectory-deployed staging URL
  4. CORS policy violations that the development proxy was silently bypassing
  5. Node.js or package version mismatches
  6. Missing build step outputs — files generated during CI that were being served from your local machine

Diagnosing the Blank Screen: The Systematic Approach

Step 1: Open the Browser Console First

The blank screen tells you nothing. The browser console tells you everything. Before doing anything else, open DevTools → Console and look for:

  • ReferenceError, TypeError, SyntaxError — JavaScript errors that prevent the app from initializing
  • Uncaught Error: Cannot find module — missing dependency or wrong import path
  • Failed to load resource: net::ERR_CONNECTION_REFUSED — API endpoint unreachable
  • Access-Control-Allow-Origin errors — CORS violations
  • Content Security Policy violations — CSP blocking scripts or styles

One of these is almost certainly present. Start with the most specific error, not the blank screen.

Step 2: Check the Network Tab

Look for requests that returned 404 or 500:

  • 404 on the main JavaScript bundle: the build output isn't where the server expects it
  • 404 on API endpoints: the API is deployed at a different path than expected
  • 500 on API endpoints: the server is running but throwing errors
  • 404 on CSS files: stylesheet paths are broken

The combination of Console errors and Network failures narrows the problem significantly.

Step 3: Compare Environment Variables

The most common single cause of "works locally, broken on staging" is environment variables. The checklist:

  • Is the variable defined in the staging environment?
  • Is the variable name exactly correct? (NEXT_PUBLIC_API_URL vs API_URL — Next.js only exposes NEXT_PUBLIC_ prefixed variables to the browser)
  • Is the value correct? A localhost URL in a production environment variable is a common mistake.
  • Is the variable being consumed correctly? process.env.VARIABLE vs import.meta.env.VARIABLE differs between CRA/Next.js and Vite.

Debugging a blank screen on staging


The Five Most Common Root Causes

1. Environment Variables in Client Bundles

Next.js only exposes environment variables prefixed with NEXT_PUBLIC_ to the browser. Vite only exposes variables prefixed with VITE_. Any variable without the correct prefix is undefined in the client bundle — silently, with no build error.

The symptom: an undefined API URL causes API calls to fail silently or to call undefined/api/endpoint, which returns a 404.

The fix: audit every process.env or import.meta.env usage in client-side code and verify the prefix is correct.

2. Base URL Mismatches

When staging deploys to a subdirectory (https://staging.company.com/app/) but development runs at root (http://localhost:3000/), relative asset paths break.

A build that generates <script src="/build/main.js"> works at localhost:3000 but returns 404 at staging.company.com/app/ because the correct path is /app/build/main.js.

The fix: configure your build tool's base or publicPath option to match the staging URL structure. Or, better, configure staging to serve the application at root.

3. CORS Policy Violations Hidden by Dev Proxy

Development servers like webpack-dev-server and Vite's dev server include proxy configurations that route API calls to the backend. This proxy bypasses CORS entirely because the request appears to come from the same origin.

On staging, the proxy doesn't exist. The browser makes cross-origin requests directly to the API, which returns CORS errors if the API isn't configured to allow the staging URL.

The symptom: API calls work in development, return CORS error on staging, causing data to not load and the application to render empty state.

The fix: configure the API server to include the staging domain in its CORS allowed origins list. This is an API configuration change, not a frontend change.

4. Package Version Mismatches

Your local machine has Node.js 20.11. The staging CI runs Node.js 18. Certain packages behave differently or aren't compatible with both versions. Certain APIs available in Node 20 don't exist in Node 18.

The symptom: build succeeds locally, fails on CI. Or worse: build succeeds on CI but produces different output than local builds.

The fix: pin Node.js version in .nvmrc, .node-version, or engines in package.json. Configure CI to use the exact version. Use npm ci instead of npm install in CI to ensure exact lockfile resolution.

5. Build Step Outputs That Don't Exist on CI

Some features require build-time generation: i18n translation files compiled from source, icons generated from SVGs, type definitions generated from GraphQL schemas. These might be committed to the repository (and thus present locally) but .gitignored (and thus absent on CI).

The symptom: a resource 404s on staging that exists locally.

The fix: move build-time generated files to the CI pipeline, or commit them to the repository consistently.


The Deployment Checklist That Prevents Blank Screens

Before deploying to any environment shared with clients:

Environment:

  • All required environment variables are defined in the deployment environment
  • Variable names match exactly (including prefix requirements)
  • No environment variables contain localhost URLs

Build:

  • Build runs successfully in CI (not just locally)
  • Node.js version is pinned and matches CI
  • npm ci or yarn --frozen-lockfile is used in CI (not npm install)
  • Build step generates all required outputs

Networking:

  • API CORS configuration includes the staging/production domain
  • API base URL in client-side code points to the correct environment
  • Asset paths are correct for the deployment URL structure

Verification:

  • Someone checks the browser console immediately after deployment
  • Someone checks the network tab for 404/500 responses on core assets
  • Someone verifies at least one core user flow end-to-end on the deployed environment

Pre-deployment checklist and verification


Making Local Environments Less Deceptive

The root cause is that local development environments hide problems that staging surfaces. The fix isn't just a checklist — it's making local environments more closely match production.

Practices that reduce the local/staging divergence:

Use Docker for backend services. Running your API, database, and Redis in Docker containers locally ensures the same runtime versions as production. No more "but it works on my machine" because of a locally-installed database version difference.

Test with production build mode locally. Run npm run build && npm run start occasionally instead of always running npm run dev. The production build has different optimizations and error behaviors than the development server.

Load environment variables from a shared source. Using a secrets manager (AWS Secrets Manager, HashiCorp Vault) for all environments — including local development — ensures that environment variables in your .env file match exactly what staging uses.

Run CI checks before PRs hit review. A CI pipeline that catches build failures, type errors, and test failures before code is merged prevents the class of "works locally, broken in staging" issues caused by untested changes.

The blank screen before a client demo is the symptom of an environment divergence that went undetected until the worst possible moment. The cure isn't a better deployment checklist — it's a development workflow that surfaces these divergences before they reach clients.

Recommended