Skip to content
Back to field notes
DeploymentJune 6, 20266 min read

Preview and staging deployments are public by default

A preview or staging deployment from Vercel, Netlify, or a similar platform is a real public URL, often running looser config and real data, and this note helps you decide what to lock down before it spreads.

DeploymentStagingAccess control

Your staging URL is a real public URL

Preview deployment public exposure starts the moment a build platform hands you a URL. On Vercel, Netlify, Cloudflare Pages, Render, and most other hosts, a preview or branch deployment gets a full, routable hostname on the public internet. There is no firewall, no VPN, and no implicit "internal only" boundary around it. If the link reaches anyone, the app answers.

That matters because the deployment that springs from a pull request or a feature branch is usually the least careful build you run. It tends to have debug output on, verbose error pages, looser CORS, a development build with source maps, and a connection to real or near-real data because that is the fastest way to test. The point of staging is to be permissive so you can see what broke. That same permissiveness is exactly what you do not want on a public surface.

Two things are worth separating here. What is observable: the URL resolves, it returns your app, and anyone who has the link can load it. What is only possible until you check: that the deployment also exposes data, secrets, or admin functionality that production would not. The first is true by default. The second is what the checks below are for.

  • A preview URL is a live production-grade endpoint, not a sandbox hidden from the world.
  • It is reachable by anyone with the link, plus crawlers and bots that find the link.
  • It frequently runs with weaker settings than your real production target.

Why AI-built apps land here

A common AI-assisted workflow is to wire the repo to a host, push a branch, and let the platform auto-deploy a preview for every commit. The tool prompt was "deploy my app," and the deploy succeeded, so the loop looks finished. What is missing is the step where someone decides whether that preview should be reachable by the public, and configures access control if not.

AI scaffolding amplifies the default in a few specific ways. Generated config rarely turns on the platform's preview protection feature, because that is an account or dashboard setting, not something a code generator reaches for. Generated apps often ship a development build to previews, which includes source maps and verbose errors. And the fast path to a working demo is to point staging at the same database, storage bucket, or API keys as production, so the preview shows real records instead of empty tables.

There is also a discovery problem that has nothing to do with anyone guessing your URL. Preview links get pasted into pull requests, issue trackers, chat tools, and analytics. From there they can end up in places that crawlers read. A preview URL indexed by Google is not a hack; it is the predictable result of a public link being linked to from another public page.

  • The deploy worked, so the missing access-control decision is invisible from the inside.
  • Preview builds default to development settings the generator never hardened.
  • Shared links leak into crawlable surfaces, so "nobody has the URL" is not a control.

Find your exposed previews before someone else does

You can confirm all of this on your own deployments without attacking anything. Run these checks against URLs you own.

List every live preview and branch deployment. In the platform dashboard, open the deployments list for your project. Vercel, Netlify, and similar hosts keep every preview build addressable, sometimes for months. The suspicious result is a long list of old preview URLs you forgot existed, each still resolving. The safer result is a short, current list, or previews that require authentication to load.

Load a preview URL in a private browser window with no login. If the app renders and shows data without asking you to authenticate, that surface is open to anyone with the link. If it instead shows a platform login wall or your own auth gate, access control is doing something. Try this from a session that has never logged in, because a logged-in cookie can mask the gap.

Check whether the preview is indexed. Search a site query for your preview hostname in a search engine, and open the deployment's robots and headers. The suspicious result is a staging environment exposed in search results, or a preview served with an X-Robots-Tag that allows indexing. The safer result is the preview blocked from indexing and, better, behind access control so indexing is moot.

Inspect the build artifacts on the public surface. Open DevTools, the Network tab, and look at what the preview ships: source maps referenced by .js.map files, verbose stack traces on error routes, and config values in the client bundle. Try fetching deployment artifacts directly, for example GET /.env or a path to /.git/HEAD, against your own preview. A 200 with file contents is a launch blocker. A 404 is the result you want. For the env-file case specifically, see public env file exposure in deployed apps.

  • Suspicious: old preview URLs still resolving, data visible with no login, preview indexed, artifacts fetchable.
  • Safer: short deployment list, an auth or platform login wall, indexing blocked, artifacts returning 404.
  • Repeat per environment. A locked production target tells you nothing about an open preview branch.

Put access control in front, and know what it skips

The durable fix is to make previews non-public by default, not to rely on the URL being secret.

Turn on the platform's preview protection. Most hosts offer a setting that puts every non-production deployment behind authentication: Vercel has deployment protection for preview URLs, Netlify has password protection and role-based access on site previews, Cloudflare Pages supports access policies on preview deployments. Enabling it means a visitor must authenticate before the app loads, which closes the "anyone with the link" gap. This is an account or project setting, so it survives redeploys in a way that a code change might not.

Separate staging from production data and secrets. Give previews their own database, their own storage bucket, and their own scoped keys, ideally with throwaway or synthetic data. Then an exposed preview leaks test records, not customer records. Ship a production build to previews so source maps and debug output do not travel with them, or strip them at build time.

Reduce the discovery surface. Add a robots rule and an X-Robots-Tag noindex header to non-production deployments so search engines do not list them, and clean up stale preview deployments you no longer need. Treat indexing controls as a way to slow crawlers, not as access control on their own.

The honest limit: preview protection plus separated data closes the public-exposure problem. It does not audit your application logic, your data access rules, or your dependencies. Those need code review.

Access control on a preview decides who can load the deployment. It does not fix what that deployment does once someone is in. A preview gated behind a login can still have a broken authorization check, a writable database table, or an admin route that any authenticated user can reach. Locking the door is authentication; deciding what each visitor may do inside is authorization, and that is a separate review.

Where VibeCodeGuard fits, and what it does not prove

VibeCodeGuard's Launch Check runs against any public URL, which includes a preview or staging deployment. Point it at the staging hostname and it inspects the same public surface it would in production: security headers, source maps that ship to the browser, exposed build and deploy artifacts like an .env or a .git directory, secret-shaped strings in the client bundle, publicly readable buckets and endpoints, and open or wildcard CORS headers. The value is that you can scan the preview before you ever promote it, so an exposed artifact shows up in your scan and not in a crawler's index.

What the scan reads is the outside surface. It flags what an unauthenticated visitor can reach and gives you severity and fix direction for each finding. It does not log into your app, it does not review your authorization rules, and it does not run a penetration test or verify that any exposure is exploitable. It also cannot tell you whether a preview should be public at all; that is your call.

Next step: open your platform's deployments list, turn on preview protection so non-production builds require authentication, and run a focused Launch Check against any preview URL before you share it. For the full pre-ship pass, work through the AI-built app launch security checklist.

A clean Launch Check on a preview means the public surface looks clean from outside. It does not prove the deployment is safe to leave public, that its data is scoped correctly, or that its auth boundaries hold for logged-in users. Treat a flagged artifact as a launch blocker, and treat a clean scan as one signal among several, not a guarantee.

> launch check

Scan the public surface before launch.

Get severity, evidence, and practical fix guidance for the checks VibeCodeGuard can run from the outside.