Preview deployments security: stop previews leaking production data
Preview deployments security means keeping Vercel and Netlify previews away from production data. Turn on deployment protection, give previews their own environment variables (the secrets and settings your app reads at runtime) and a separate staging database, and add a noindex tag, which tells search engines to skip a page. A public preview wired to production is a live copy of your app anyone with the link can use.
QUICK CHECKLIST
- Turn on deployment protection for previews
- Give Preview its own env var values
- Use a separate staging database and test keys
- Seed staging with fake data only
- Add noindex to every non-production build
- Redeploy after changing variables
Why preview deployments security gets skipped
On Vercel, every push to a branch that isn't your production branch gets its own Preview Deployment URL. On Netlify, pull requests get Deploy Previews, and branches you enable get branch deploys.
Those URLs run your real server code with whatever environment variables the host gave them. If you pasted your .env into the dashboard once and ticked every environment, that includes your live database URL and live API keys. Few people think of a preview as a real app, so few people lock it down.
The fix has three layers, and each one covers a gap the others leave:
- Limit who can open the preview URL.
- Limit which secrets the preview receives.
- Keep real customer data out of your staging environment (a copy of your app used for testing).
How a public preview leaks real data
Preview URLs travel. They land in pull request comments, Slack threads, tickets and screenshots. A staging.yourapp.com domain with its own HTTPS certificate shows up in public certificate transparency logs (open records of every certificate issued), which people search on purpose. Someone can find the URL without anyone sharing it.
Once someone has the URL, they use it like any other app. They sign up, call your API routes and try your forms. If that preview talks to your production database, they're doing all of it against real rows.
Staging also collects shortcuts: a login bypass for demos, a debug route, verbose error pages. Those are tolerable behind a login wall and dangerous on an open URL.
If a search engine crawler (the bot that scans sites for search results) finds a public staging URL, a duplicate of your site can end up in results, including pages you never meant to show. Protection and noindex together prevent that.
Vercel also has a Protected Source Maps option, described in the Deployment Protection docs. Source maps are files that map your minified browser code back to your original source. This option puts .map file requests behind Vercel Authentication, so shipped source maps don't expose your source code.
Before and after: previews wired to production
This is the common setup. One set of variables, applied to every environment, so the preview build uses your live database and your live Stripe key.
# Vercel > Project > Settings > Environment Variables DATABASE_URL postgres://USER:PASS@prod-host/app Production, Preview, Development STRIPE_SECRET_KEY sk_live_xxx Production, Preview, Development SUPABASE_URL https://PROD_REF.supabase.co Production, Preview, Development SUPABASE_SERVICE_ROLE_KEY PROD_SERVICE_ROLE_KEY Production, Preview, Development NEXT_PUBLIC_SUPABASE_ANON_KEY PROD_ANON_KEY Production, Preview, Development
# Vercel > Project > Settings > Environment Variables DATABASE_URL postgres://USER:PASS@prod-host/app Production DATABASE_URL postgres://USER:PASS@staging-host/app Preview, Development STRIPE_SECRET_KEY sk_live_xxx Production STRIPE_SECRET_KEY sk_test_xxx Preview, Development SUPABASE_URL https://PROD_REF.supabase.co Production SUPABASE_URL https://STAGING_REF.supabase.co Preview, Development SUPABASE_SERVICE_ROLE_KEY PROD_SERVICE_ROLE_KEY Production SUPABASE_SERVICE_ROLE_KEY STAGING_SERVICE_ROLE_KEY Preview, Development NEXT_PUBLIC_SUPABASE_ANON_KEY PROD_ANON_KEY Production NEXT_PUBLIC_SUPABASE_ANON_KEY STAGING_ANON_KEY Preview, Development APP_ENV production Production
Vercel:
- You pick one or more environments for each variable.
- Preview environment variables apply to deployments from any branch that isn't the Production Branch.
- You can override a variable for a single branch.
Netlify:
- Deploy contexts do the same job, and you set them in the UI.
- In
netlify.tomlyou can use blocks like[context.deploy-preview.environment], but that file lives in your repo, so keep secrets out of it.
Next.js copies any variable with a NEXT_PUBLIC_ prefix into the JavaScript your browser downloads. A preview that sets production values there gives them to every visitor. See NEXT_PUBLIC_ environment variables for which keys can live there.
How to fix preview deployments security, step by step
Start with the dashboard settings, then the code.
Turn on protection for previews
On Vercel, open your project, go to Settings, then Deployment Protection:
- Standard Protection covers all deployments except production domains and is available on all plans.
- Pair it with Vercel Authentication, which lets only Vercel users with access rights open previews.
On Netlify, use password protection or team login protection for Deploy Previews and branch deploys, or make the project private. Plan limits apply, so check the current docs.
Scope variables per environment
Open your project's environment variables. Keep the live value in Production only and add a separate value for Preview (and Development if you run
vercel dev). On Netlify, set the scope and deploy context for each variable in the UI. Redeploy afterward (see step 6). If a.envfile ever got committed, read .env file leaks on Vercel and Netlify.Create a separate staging backend
Make a second Supabase project, a second Firebase project or a second database. Use Stripe test keys and sandbox keys for every other service. Copy your schema and access rules over with migrations (versioned database change scripts) so staging has the same Supabase RLS (row-level security) policies or Firebase security rules as production.
Add noindex outside production
Set
APP_ENV=productionin the Production environment only. Anything else, including a forgotten variable, getsnoindex. This only asks well-behaved crawlers to skip the page, so protection from step 1 still does the real work.// app/layout.tsx import type { Metadata } from "next"; import type { ReactNode } from "react"; export const metadata: Metadata = { robots: process.env.APP_ENV === "production" ? { index: true, follow: true } : { index: false, follow: false }, }; export default function RootLayout({ children }: { children: ReactNode }) { return ( <html lang="en"> <body>{children}</body> </html> ); }Fill staging with fake data
Seed staging with made-up users and orders. Don't restore a production dump into it, even to reproduce one bug. Staging has weaker access control, so real emails and addresses end up where more people and tools can read them.
Redeploy, clean up and rotate
Environment variable changes don't touch previous deployments, so old previews keep the values they were built with and stay reachable at their own URLs. Redeploy the branches you still use. Then delete old preview deployments that ran with production values, or confirm that protection covers them. If production keys ever ran in a public preview, rotate them. The leaked API keys guide shows how.
Test it yourself
Grab a preview URL from a pull request or your dashboard. First check whether it's open to strangers:
curl -s -o /dev/null -w "%{http_code}\n" https://YOUR-PREVIEW-URL/A bad result is 200 and your app's HTML. A good result is a login or password page, or a 401 or similar status. The exact response depends on the protection method.
Next, check which backend the preview uses:
- Sign up in the preview with a throwaway email.
- Open your production dashboard (Supabase Table Editor, Firebase console or your database client) and look for that user.
- If the user is there, the preview writes to your production database. Delete the test row and redo steps 2 and 3 of the fix above.
Then check noindex. Run these two commands:
curl -s https://YOUR-PREVIEW-URL/ | grep -i "robots" curl -sI https://YOUR-PREVIEW-URL/ | grep -i "x-robots-tag"
Good is a <meta name="robots" content="noindex, nofollow"/> tag or an X-Robots-Tag: noindex header in your app's own page. If protection is on, curl output comes from the login page, so don't count it. Check the page source in a logged-in browser instead. Bad is no robots tag in your app's HTML.
Last, search for yourself. Try site:yourapp.com staging and a search for your app name plus vercel.app or netlify.app. If preview pages show up, they were public long enough to be crawled. An empty result is inconclusive, since crawlers may not have found them yet.
Fix prompt you can paste
Give this to Cursor or Claude Code at the root of your repo.
Audit this repo for preview and staging exposure on Vercel or Netlify. 1. List every env var read through process.env or import.meta.env. Mark which are secrets (database URLs, Supabase service role keys, Stripe secret keys, third-party API keys). Output file and line for each. 2. Search code, vercel.json, netlify.toml and config files for hard-coded production URLs, Supabase project refs and Firebase project IDs. Output file and line. 3. Add robots noindex metadata to the root layout whenever APP_ENV is not "production", so a missing variable still gets noindex. Output the file and line you changed. 4. Find staging-only shortcuts such as auth bypasses, debug routes, ?debug flags, seeded admin users, and list them with file and line. 5. Find any NEXT_PUBLIC_ variables or other browser-exposed values that hold secrets or production URLs, and any committed .env files. Output file and line for each. 6. Write a table of env vars I must set separately for Production and Preview in my host dashboard. Use placeholders only. Never print or invent real secret values. Do not change any secret values. For every change, give the file and line, the old code, the new code and one sentence on why. End with a list of dashboard steps I still have to do by hand.
Vercel and Netlify protection options compared
Pick the narrowest setup that fits who needs to see your previews. A solo founder can use Vercel Authentication or a Netlify private project. A team that shows previews to clients may need a shared password or SSO.
| Vercel | Netlify | |
|---|---|---|
| Protect previews only | Standard Protection (all plans) | Previews only or non-production only (plan dependent) |
| Protect production too | All Deployments (all plans) | Protect all deploys |
| Login based option | Vercel Authentication | Team login or private project |
| Shared password | Password Protection (Pro, not Hobby) | Basic password (plan dependent) |
| Variable scopes | Production, Preview, Development, custom | Deploy contexts, set in UI or netlify.toml |
Check each vendor's current docs for plan limits and prices.
Netlify team login covers Developers, Team Owners and Billing Admins, so Git Contributors can't open protected deploys. Single sign-on (SSO) logins on Netlify last 1 hour, so a long review session can hit a 401 (unauthorized) error.
The vibe coding security checklist covers the other checks to run before launch. If you want another check on a deployed URL, Leakcade can scan it.
Further reading
- Vercel docs: Deployment Protection
- Vercel docs: Environment variables
- Netlify docs: Password Protection overview
- Netlify docs: Get started with environment variables
FAQ
How do I password protect Netlify deploy previews?
In your Netlify project settings, open Password Protection and choose all deploys, or (on Enterprise) only Deploy Previews and branch deploys. On Credit-based Pro plans the setting is called Project visibility, with a Password option and a Previews only scope. Credit-based Free and Personal plans have no password option, so make the project private instead.
Are Vercel preview deployments public?
They can be. Vercel Deployment Protection controls who can open your preview and production URLs, and you configure it per project. Standard Protection is available on all plans and covers everything except production domains. To check, open a preview URL in a private browser window. If your app loads without a login, it's public. Then review your project's Deployment Protection settings, because team defaults can differ.
Should staging use the production database?
No. Anyone who can reach the staging app can use its routes against real data, and a staging bug can write bad rows into production. Use a separate database or project with fake data.
Do preview deployments use my production environment variables?
Only if the variable applies to the Preview environment, which includes team-level variables on Vercel, shared variables on Netlify, and netlify.toml values, which override the UI. Vercel lets you pick Production, Preview or Development for each variable. In Settings, then Environment Variables, check that every secret either excludes Preview or has its own Preview value, such as a test key or a staging database URL.
How do I stop Google indexing my staging site?
Put it behind a login or password first, since a login wall stops crawlers from reading the page. Then add a noindex meta tag or an X-Robots-Tag header on every non-production build. A robots.txt file alone isn't enough, because it only asks crawlers not to crawl and doesn't secure anything.
Want a second pair of eyes?
We review your whole repo and write a fix prompt for every finding. $19, report within an hour.
Check my repoLeakcade is an automated check. It can miss vulnerabilities and is not a substitute for a professional security audit or pentest. Provided as is. You stay responsible for your code. Where the law allows, our liability is limited to what you paid ($19). Terms