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.
- 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:
- In Zero Trust, create a self-hosted application for
*.preview.example.com. - Add a policy: Allow where the email ends in
@example.com, or where the user belongs to an identity provider group such asengineeringorcontent. - Add a second policy for external reviewers if needed — Allow specific email addresses with one-time PIN login — and give it an expiry.
- 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-IdandCF-Access-Client-Secretheaders. - Vercel: generate a Protection Bypass for Automation secret; CI sends it in the
x-vercel-protection-bypassheader. - Basic auth: give CI its own password or header token, checked separately in the edge function.
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.
Link Unfurling and Sharing
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.
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.
Related
- Parent: Preview Environments for Pull Requests — the full preview workflow.
- Cleaning Up Stale Preview Deployments — fewer previews to protect.
- Previewing Headless CMS Drafts — where the most sensitive content appears.
- Protecting a Static Site Behind Authentication — the same techniques for production.
- Visual Regression Testing on Preview Deploys — tests that need the bypass token.