If you operate a live web app with paying customers, a customer screenshot of a blank page creates an immediate production question. The blank screen in production is real, but the image does not tell you what failed. Before anyone changes the live app, you can capture a reproduction record that separates at least three credible candidates. Forwarding the screenshot or redeploying feels sensible, yet each move leaves the cause unresolved. The same empty frame can come from build configuration, an outdated bundle, or a data request that returned nothing, depending on how the application handles each failure.
Every platform claim below was checked against vendor documentation on 2026-09-23.
TL;DR
- A screenshot establishes that the screen was blank; it does not identify which build, account, request, or session produced that result.
- Build configuration, version skew, and an empty data response can produce the same symptom, and two can leave no useful error string on screen when the app renders no fallback.
- Capture the eight-item reproduction record before redeploying, so the next decision follows preserved evidence rather than a guessed fix.
Why sending the screenshot is the right instinct
A screenshot is evidence. It documents that a customer reported the failure and helps end an unhelpful argument about whether the problem exists.
For a visual defect, such as a clipped button or misaligned card, the image may be almost the whole report. Forwarding images works often enough to become a reasonable habit.
This post covers the narrower case where a page renders no useful content and the cause is not visible in the frame. Here, the screenshot records the symptom while leaving the producing path unknown.
What the screenshot actually establishes
A typical customer screenshot shows what appeared on screen and roughly when someone noticed it. It commonly omits the exact URL, deployed build, affected account, and browser console and network record.
Those details help distinguish a shared production failure from an account-specific result, and an old bundle from the current deployment. Even a timestamp without a timezone can point at the wrong release window.
Vite’s troubleshooting documentation assigns one exact error string to three documented causes: version skew, poor network conditions, and browser extensions that block requests. If a precise message does not name one cause, a frame containing no message cannot do so either.
One blank screen in production, at least three causes
This pattern has credible candidates in three layers: the build did not include the configuration, the browser is running the wrong bundle, or the bundle ran and the data layer returned no rows. These are candidates, not a fix order. The mechanisms below are documented separately by Vite, Vercel, and Supabase (Vite env documentation, Vite troubleshooting, Supabase empty-array troubleshooting).
The build did not include the configuration
In Vite, import.meta.env values are statically replaced at build time. Vite writes client-side variables that use the configured VITE_ prefix into the bundle rather than reading them for each browser session. That is how env values get baked into the build.
Deployment platforms can add another configuration layer. On Vercel, for example, environment variables are scoped to Production, Preview, Development, and optional custom environments. The same name can hold different values across those environments. Vercel also documents that an environment-variable change applies to new deployments, not previous deployments.
A missing import.meta.env property is replaced as undefined at build time (Vite env documentation); nothing in that path writes a console message naming configuration as the cause. What the visitor sees next depends on the application: render no fallback for the undefined value and the screen stays empty.
The bundle is the wrong one
When a new deployment removes old chunk files, an older cached page can request a dynamically imported JavaScript chunk that the current release no longer serves. Vite documents this as version skew: the cached page and current release expect different asset versions (Vite troubleshooting). With no fallback for the failed import, the page stays blank after the deploy while the console shows:
TypeError: Failed to fetch dynamically imported module
The message is useful but not decisive. Vite’s troubleshooting page lists version skew, poor network conditions, and browser extensions that block requests as causes of this same string.
If Next.js replaces the page with its production error shell instead of leaving it empty, use the application error and client-side exception after deploy diagnosis to preserve the first Console error and failed request.
The error alone does not prove an old bundle is responsible. A blank React or Next.js page after a deploy can still have another cause; build identity, request evidence, and deploy timing are needed before naming version skew. Vite also tells framework users to refer to their framework documentation first because a framework may have its own handling for this problem (Vite troubleshooting).
The bundle ran; the data layer returned no rows
Row-level security, or RLS, is a database policy system that limits which rows an operation can affect. Supabase’s RLS documentation explains that policies act as conditions on table access and that auth.uid() returns the ID of the user making the request.
Supabase documents that a select returning an empty data array usually means RLS is enabled without a matching policy, or the user does not meet that policy.
The same document lists a filter with no matching rows as another cause of the identical empty result. A successful request with an empty array can therefore mean either that the account has no matching data or that no rows survived the access policy. That is why an empty result is not an error. Supabase also documents that an unauthenticated request — including an expired session — makes auth.uid() return null, so a policy based on auth.uid() may match no rows (Supabase RLS documentation). In these cases there may be no error string to search for.
Do not disable a live policy to distinguish those cases. A second known-good account is useful only when it uses the same URL, browser state, and build, and is known to have matching data. That comparison can show that the result is account-specific, but it cannot by itself distinguish correct no-data behavior from a wrong policy or missing session. Check the expected rows, session, and policy on a non-production copy. The same blind spot shows up before launch, where every request during the build carried the builder’s own session — nine preview gaps covers that case. If it suggests an isolation problem, it belongs in the RLS and data-isolation review.
Why the symptom does not identify the cause
Three paths can end in one empty frame. Seeing the frame does not reveal which path was taken. Vite’s one-message-three-causes documentation shows the same limitation even when more evidence than a screenshot is available (Vite troubleshooting).
Guessing has an operational cost. A configuration deploy may leave a stale-client problem untouched. A policy change aimed at an empty response may be irrelevant. Each attempt adds another production state to explain.
A stale chunk request and an environment-variable change are separate mechanisms with separate fixes (Vite troubleshooting, Vercel environment-variable documentation), so a redeploy that clears the symptom is not evidence of which one produced the report. Capture first; change second.
What to capture instead: the reproduction record
Capture the conditions before changing production
Do this before a redeploy, rollback, account reset, or policy change: to reproduce a production bug later, you need these conditions preserved before the failing state changes. It is also what a developer needs from a bug report — the conditions, the evidence and the comparison results, not only the screenshot. Copy the block into a private incident note and fill every line you can.
BLANK-SCREEN REPRODUCTION RECORD
1. Account label (not the customer's email):
2. Exact page URL:
3. Timestamp and timezone:
4. Console contents, including "no messages" if empty:
5. First relevant failed app request; if none failed, first app data request with status and redacted response shape:
6. Private-window result: reproduces / does not reproduce / not tested
7. Second known-good account on the same URL and build: reproduces / does not reproduce / not tested
8. Last deploy time and visible build identifier, if available:
Read the record as evidence, not a verdict
Use a neutral label such as affected-account-A. Its presence lets an engineer compare access conditions; without it, account-specific and shared failures remain mixed. The exact URL separates one route from the whole app. Time and timezone tie the report to a deploy and available logs.
Open the browser’s developer tools on the affected page. Keep the Console and Network panels open. Reload once. Record the first relevant failed application request, including its URL, status, and resource type. If none failed, record the first application data request and whether its redacted response shape was empty, because Supabase documents successful select calls that return an empty data array when no rows match the filter or access policy (Supabase empty-array troubleshooting). Do not copy tokens, headers, or customer data.
If the first failed request returns 502, the blank page is only the browser symptom. Use the 502 Bad Gateway diagnosis for a live web app to find whether the request stopped at Vercel, a Next.js function, Supabase, Railway or another upstream service.
Chrome documents that an Incognito window starts a separate browsing session and does not automatically sign the user in to websites. Chrome also documents that extensions run in Incognito only when they are explicitly allowed there (Chrome extension help).
That means the test changes several variables at once, including browser session state and potentially extension behavior. Treat it as a broad browser-profile check, not proof that the session alone caused the result.
A second known-good account is useful only when the route, build, browser conditions, and expected data are held constant. The deploy time and build identifier help test whether the report belongs to the release you think is live. A missing result leaves that branch open.
The record passes intake when all eight lines have a value, including “not tested” or “none failed,” and the browser capture happened before production changed. It fails intake when account, URL, time, or pre-change browser evidence is missing. That means the record is incomplete, not that the incident is disproved.
Before forwarding anything, remove JWTs, session tokens, project references, customer emails, and customer data. Apply the same rule to screenshots, console exports, and network captures; how this desk handles access and evidence explains the boundary. The bug fix intake says the same: no passwords, API keys, tokens, private keys, card details or customer-data exports.
| Candidate | Documented evidence | What incident evidence can narrow it |
|---|---|---|
| Build-time configuration | Vite documents build-time replacement and shows a client-exposed variable resolving to a value while an unavailable property resolves to undefined (Vite env documentation). |
Compare the affected build’s configured variable name, environment scope, and build-time value without exposing secrets. |
| Wrong bundle / version skew | Vite documents the dynamic-import error and version skew caused by cached HTML requesting chunks removed by a later deployment (Vite troubleshooting). Vercel separately documents version-skew failures where older clients request assets that a newer deployment no longer serves (Vercel version skew protection). | A failed old-chunk request plus deployment/build identity supports version skew, but the error alone does not exclude network failure or extension blocking. |
| Empty data result | Supabase documents RLS/policy mismatch and an ordinary no-match filter as causes of an empty array, and documents that an unauthenticated or expired session makes auth.uid() return null (Supabase empty-array troubleshooting, Supabase RLS documentation). |
Compare expected rows, authenticated session state, query filters, and policy outcome in a safe test environment. |
A complete reproduction record can eliminate candidates and show which ones remain plausible. It does not guarantee a single cause or guarantee that the failure reproduces on demand — some causes are tied to one session, one device, or one deploy that has already been replaced, and those are narrowed by elimination rather than re-run.
What actually closes the gap
The record helps with today’s incident. The changes below are development work for the application’s development team or maintainer; they are not part of the incident-restoration work this desk takes on. Four engineering changes can make the next one easier to interpret:
- Render an error boundary. In a React app, an Error Boundary can replace a child component’s rendering failure with fallback UI. React documents that it does not catch event-handler errors, server-side rendering errors, errors thrown inside the boundary itself, or most asynchronous callback errors.
- Display or record a deployment identifier. Where the deployment platform provides one, record a deployment ID or commit SHA with the incident. Vercel documents
VERCEL_DEPLOYMENT_IDas a unique deployment identifier andVERCEL_GIT_COMMIT_SHAas the Git commit SHA associated with the deployment; Netlify documentsDEPLOY_ID,BUILD_ID, andCOMMIT_REFfor the same kind of deployment/build traceability (Vercel system environment variables, Netlify build environment variables). These identifiers can tie a report to a deployment or source revision, but the exact mechanism is platform-specific. - Surface session state safely. A signed-in, signed-out, or expired state is more useful than an empty frame when the affected data path depends on authentication. Supabase documents that an expired or missing session makes
auth.uid()returnnullfor RLS evaluation (Supabase RLS documentation). Do not expose tokens or customer identifiers. - Capture client-side errors. React provides root-level callbacks for caught, uncaught, and recoverable errors, and
componentDidCatchcan be used to log an Error Boundary’s caught render error (ReactcreateRoot, React Component). Any external reporting vendor adds a separate storage and privacy boundary to review.
These changes narrow diagnosis; none of them addresses the three causes above — an error boundary still cannot repair a missing configuration value, restore a deleted chunk or correct an RLS policy. They make the next failure legible, not less likely.
How we’d approach this
A screenshot and “it worked last week” are the starting point, not the diagnosis. Before anything in production changes, we establish the last-known-good state and reproduce the failure against a named account and a named build. We then separate bundle configuration, bundle identity, and the data layer. The bug fixing service runs the same sequence: reproduce, diagnose with evidence, then change and test within the agreed scope.
This maps to the general web-app bug fixing service, not an open-ended rebuild. The desk currently takes on React, Next.js, TypeScript, Node.js, Supabase, PostgreSQL, Stripe, and the authentication and deployment around them. WordPress and other CMS, PHP/Laravel, Rails, MERN-specific work, native iOS/Android, new apps or features, visual redesign, penetration testing, and live breach response are outside the current scope.
If what looks like a blank screen turns out to be a suspected live data exposure rather than a broken render, that is a security-incident specialist’s job. The evidence should not go through a public contact form. If the screen needs rebuilding from scratch rather than restoring, that is development work, not incident restoration.
The image names the symptom; the reproduction record preserves the conditions needed to investigate the cause. Sometimes it narrows three candidates to one, and sometimes it shows which evidence is missing. It cannot recover a bundle or session replaced before capture. That limitation is why the order matters.
If a screen that worked before is blank for your customers and the record above still leaves multiple candidates open, send the bug fix request. It starts with a fixed-scope diagnosis and the current repair bands are published on the bug fixing service page — scope and price agreed before anything in production changes.
If the application was built with an AI builder such as Lovable, Bolt, or v0, use the more specific AI-built app fix entry point instead; the general bug fixing service routes those applications there.