LEAKCADE

Preview deployments security: stop previews leaking production data

Updated 8 min read

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.

How a preview reaches production dataA preview URL leaks through a pull request or chat. It is open to anyone with the link, it uses production environment variables, and so it reads and writes real customer rows in the production database.LINKPreview URLshared in a PR orchatPREVIEWOpen to anyonewith the URLENVReuses productionenv varsDATABASEReal customerrows exposedLINKPreview URL shared in a PR or chatPREVIEWOpen to anyone with the URLENVReuses production env varsDATABASEReal customer rows exposed
One leaked link is enough when the preview shares production secrets.

The fix has three layers, and each one covers a gap the others leave:

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.

BEFOREWrong: every variable in every environment
# 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
AFTERRight: separate values for Preview
# 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
Same variable names, different values per environment. Preview gets test and staging values.

Vercel:

Netlify:

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.

  1. 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.

  2. 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 .env file ever got committed, read .env file leaks on Vercel and Netlify.

  3. 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.

  4. Add noindex outside production

    Set APP_ENV=production in the Production environment only. Anything else, including a forgotten variable, gets noindex. 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>
      );
    }
  5. 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.

  6. 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:

  1. Sign up in the preview with a throwaway email.
  2. Open your production dashboard (Supabase Table Editor, Firebase console or your database client) and look for that user.
  3. 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.

FIX PROMPT
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.

VercelNetlify
Protect previews onlyStandard Protection (all plans)Previews only or non-production only (plan dependent)
Protect production tooAll Deployments (all plans)Protect all deploys
Login based optionVercel AuthenticationTeam login or private project
Shared passwordPassword Protection (Pro, not Hobby)Basic password (plan dependent)
Variable scopesProduction, Preview, Development, customDeploy 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

  1. Vercel docs: Deployment Protection
  2. Vercel docs: Environment variables
  3. Netlify docs: Password Protection overview
  4. 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 repo

Leakcade 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

Patch Notes by Leakcade

A short weekly email: what changed in AI coding tools, one real security mistake, one prompt you can reuse.