Password-Protecting Preview Deployments

Preview deployments are one of the best things about modern static hosting: every pull request gets its own URL, reviewers see the real site, and nobody has to run a build locally. They are also a quiet leak. A preview for a launch announcement is live on a public URL days before the launch. It gets pasted into a chat channel with external guests, unfurled with a screenshot, crawled because someone linked it from a public issue, and indexed by a search engine that then shows the unreleased page for weeks.

This guide covers the ways to put previews behind authentication — platform-native protection, Cloudflare Access, and a small edge function for hosts that offer neither — plus how automated tests get through and how to keep previews out of search indexes regardless. It is part of Preview Environments for Pull Requests.

Prerequisites

  • Preview deployments already running for pull requests — see Automating Preview Deploy Pipelines with GitHub Actions.
  • Admin access to the hosting project, and to your identity provider if using SSO.
  • A list of who needs access: the team, reviewers in other departments, occasionally external stakeholders.

Choosing a Mechanism

The right mechanism depends on how sensitive preview content is and who needs to see it.

Preview protection options by strength and effort Four options arranged from weakest to strongest. noindex header only: stops indexing, no access control. Shared basic auth password: simple barrier, cannot revoke per person. Platform protection such as Vercel Authentication or Netlify password protection: team login, some plans only. Identity-based access such as Cloudflare Access with SSO: per-person access, audit log, service tokens for CI. From hiding to controlling access weaker stronger noindex only hides from search no access control shared password basic auth at the edge no per-person revoke platform protection team login on host plan-dependent identity access SSO per person audit log, tokens
noindex belongs on every preview; the question is what to add on top.
  • Platform protection is the least work where available. Vercel's Deployment Protection requires a Vercel login from a team member for previews by default, and offers password protection and trusted IPs on higher plans. Netlify offers site-wide password protection and team login for deploy previews on paid plans. Cloudflare Pages can put preview deployments behind Cloudflare Access with one setting.
  • Cloudflare Access (or a similar zero-trust proxy) works in front of any host whose previews are on a domain you control, such as *.preview.example.com. People sign in with the company identity provider, access is granted by group, and every login is logged.
  • An edge function with basic auth is the fallback for hosts with no protection features. It is simple and adequate for low-sensitivity content.

Cloudflare Access for Preview Domains

If previews live on a wildcard subdomain in a Cloudflare zone, one Access application covers all of them:

  1. In Zero Trust, create a self-hosted application for *.preview.example.com.
  2. Add a policy: Allow where the email ends in @example.com, or where the user belongs to an identity provider group such as engineering or content.
  3. Add a second policy for external reviewers if needed — Allow specific email addresses with one-time PIN login — and give it an expiry.
  4. For Cloudflare Pages previews on *.project.pages.dev, enable Access policy for preview deployments in the project settings instead; it creates the application for you.

Set the session duration to match how reviewers work — a day is usually comfortable — so people are not asked to sign in for every preview link in a busy review week. Because the application covers the whole wildcard, one login grants access to every preview until the session expires.

Access sits in front of the preview at the edge. Unauthenticated requests are redirected to the login page and never reach the site, so no preview content — not even its HTML title — is exposed.

Basic Auth in an Edge Function

For hosts without built-in protection, a few lines at the edge add a password. On Cloudflare Workers:

export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);
    const isPreview = url.hostname !== 'www.example.com';
    if (isPreview) {
      const expected = 'Basic ' + btoa(`preview:${env.PREVIEW_PASSWORD}`);
      if (request.headers.get('Authorization') !== expected) {
        return new Response('Authentication required', {
          status: 401,
          headers: { 'WWW-Authenticate': 'Basic realm="Preview", charset="UTF-8"' },
        });
      }
    }
    const res = await env.ASSETS.fetch(request);
    const out = new Response(res.body, res);
    if (isPreview) out.headers.set('X-Robots-Tag', 'noindex, nofollow');
    return out;
  },
};

Netlify Edge Functions and Vercel Middleware can do the same. Store the password as a secret, rotate it when people leave, and compare in constant time if the preview protects anything valuable. Remember that basic auth sends the password with every request, so it must only ever be served over HTTPS.

Letting CI and Bots Through

Protected previews break the automated checks that run against them — link checks, visual regression tests, Lighthouse — unless they have a way in. Use the mechanism each option provides for automation, never a real person's credentials:

  • Cloudflare Access: create a service token and add a policy with the Service Auth action. CI sends CF-Access-Client-Id and CF-Access-Client-Secret headers.
  • Vercel: generate a Protection Bypass for Automation secret; CI sends it in the x-vercel-protection-bypass header.
  • Basic auth: give CI its own password or header token, checked separately in the edge function.
How people and CI reach a protected preview A reviewer's browser is redirected to the company identity provider, signs in, and receives a session cookie that lets it load the preview. The CI test runner sends a service token in request headers and is allowed through without a login. An anonymous visitor with the URL receives a login page and never sees the preview content. Three visitors, one preview URL reviewer (SSO) CI runner (token) anyone with the URL edge access check session cookie? service token? otherwise: login page preview content login page only
Automation gets its own revocable credential; nobody shares a person's login.

Store tokens as CI secrets available only to the workflows that need them. Smoke tests against previews are covered in Running Smoke Tests Against a Preview URL.

Chat tools fetch URLs to build previews. A protected preview returns a login page, so unfurls show "Sign in" rather than the content — which is the point. If reviewers want meaningful unfurls, share screenshots from the visual regression job instead of making the preview public. Similarly, protected previews will not show real Open Graph images when shared; test those on production after launch or with a platform debugger that supports authentication headers.

Noindex on Every Preview

Whatever else you do, send X-Robots-Tag: noindex on every preview response and make sure preview builds do not emit a sitemap pointing at production URLs or a canonical tag pointing at themselves. If protection is ever turned off by mistake, noindex keeps the previews out of search results. Also keep previews on a separate hostname from production, never a path, so a single header rule or Access application can cover them without risk to the live site.

Indexed preview URLs before and after protection A site search for the preview domain returned 1,240 indexed preview URLs before protection. Eight weeks after adding Cloudflare Access and noindex, it returned 12, and none after twelve weeks. Preview URLs found in a search engine index 1,240 before 12 week 8 0 week 12 removal requests were filed for the 40 most sensitive URLs to speed things up
Indexed previews drain out over weeks once crawlers see noindex and login pages.

Measured Impact

A company discovered 1,240 of its preview URLs in a search engine's index, including an unreleased pricing page. It moved previews from the host's public preview domain to *.preview.example.com behind Cloudflare Access with SSO, added service tokens for its Playwright and link-check jobs, and set X-Robots-Tag: noindex on every preview response. Indexed preview URLs fell to 12 after eight weeks and to zero after twelve, helped by removal requests for the most sensitive pages. Reviewers' feedback was that SSO was less friction than the shared password it replaced.

Pitfalls & Rollback

  • Protecting the preview but not its assets. Images and JSON on another hostname can still leak; cover every preview hostname.
  • Shared passwords in chat. They end up everywhere; prefer identity-based access.
  • Breaking CI. Add the service token path before turning protection on, and test it against one preview first.
  • Previews on production paths. /preview/... on the live domain is hard to protect safely; use separate hostnames.
  • Rollback: disable the Access application or the protection setting; noindex remains as a safety net.

Conclusion

Preview deployments should be private by default. Add X-Robots-Tag: noindex everywhere, then put previews behind the strongest access control your platform offers — built-in team login, Cloudflare Access with SSO, or at minimum basic auth at the edge — and give automated checks their own revocable tokens. Keep previews on separate hostnames so one rule covers them all, and a leaked preview URL becomes a login page instead of an early launch.

FAQ

Why should preview deployments be protected?

Previews often contain unreleased features, draft announcements, pricing changes or content under embargo, and their URLs leak through chat, screenshots, referrer headers and link unfurling. Protection stops anyone who finds a URL from reading it and stops search engines from indexing it.

Is HTTP basic auth good enough for previews?

For low-sensitivity previews, a shared password over HTTPS is a reasonable barrier. It is weak for anything confidential because the password is shared, rarely rotated and cannot be revoked per person. Use identity-based access such as Cloudflare Access or platform SSO for sensitive content.

How do automated tests reach a protected preview?

Use a bypass mechanism designed for automation - a service token with Cloudflare Access, a protection bypass secret header on Vercel, or a separate credential for basic auth - stored as a CI secret and sent only by the test runner.

Does noindex replace password protection?

No. noindex only asks well-behaved crawlers not to index a page; anyone with the URL can still read it. Use both - protection to control access, and an X-Robots-Tag noindex header as a safety net if protection is ever turned off.