The first time I pushed a staging build live on Netlify, I thought everything looked flawless — until during testing I noticed production users were suddenly hitting staging APIs. If you’ve ever felt your stomach drop at that kind of mistake, you know how real “configuration bleed” is. This article is my attempt to distill hard lessons learned into a strategy for keeping staging, production, and everything in between completely isolated. After experiencing these mishaps managing deployments on Netlify, I have learned the platform’s flexibility can become a liability when we don’t establish clear boundaries between environments.

Configuration bleed is not just an inconvenience - it’s a security vulnerability waiting to happen. With this misses you might accidentally expose staging API keys in production, leak sensitive user data between environments, and push untested features to live users.

What is Netlify?

Netlify is essentially a deployment pipeline bundled into a developer-friendly platform. At its core, it takes your code from a Git branch, runs your build commands, and instantly publishes a live version of the app. What makes it attractive to many teams is the removal of infrastructure headaches — you don’t manage servers or worry about CI integrations. Instead, you get a system where commits can translate almost immediately into working previews or production-ready deployments.

The Hidden Dangers of Poor Environment Separation:

When environments blur together, the problems don’t just stay technical — they ripple into security and user trust. A few scenarios I’ve personally seen (or narrowly avoided):

From a maintenance perspective, configuration bleed creates operational nightmares:

The Root Problem: Single-Site Thinking

As a developer when we start with Netlify we create a single site connected to the main repo branch. This works perfectly for simple projects, but as applications grow and the complexity grows, this approach becomes problematic. The inclination will be to use Netlify’s context-based configuration to handle different environments within the same site, but this creates shared state that inevitably leads to configuration bleed.

The fundamental issue is treating environments as configurations rather than completely separate deployments. As a developer when one thinks of staging and production as different versions of the same site, naturally they will inherit all the coupling problems that come with the shared infrastructure.

The Solution: True Environment Isolation

After countless hours debugging configuration issues, I have settled on a simple principle: separate sites per environment. This means creating distinct Netlify sites for production, staging, and any other long-lived environments you need.

Here’s why this approach is superior:

Security Benefits:

Maintenance Advantages:

Recommended Architecture:

Site Structure:

Create two separate Netlify sites:

  1. Production site: connected to main or production branch
  2. Staging site: connected to staging or develop branch

Each site should have its own:

Configuration Strategy:

Here’s a stripped-down netlify.toml example I use in a monorepo. It’s adapted from Netlify’s own docs, but tweaked to clarify environment-specific overrides:

toml

[build]
#Set frontend app folder (for monorepos)
base = “frontend”
command = “npm ci && npm run build”
publish = “frontend/dist”

[build.environment]
NODE_VERSION = “20”

#Environment-specific API endpoints: 
[context.production.environment]
API_BASE_URL = "https://api.example.com"
FRONTEND_ORIGIN = "https://app.example.com"

[context.staging.environment]
API_BASE_URL = "https://api-staging.example.com"
FRONTEND_ORIGIN = "https://staging.example.com"

[context.deploy-preview.environment]
API_BASE_URL = "https://api-preview.example.com"
FRONTEND_ORIGIN = "https://deploy-preview-<id>--example.netlify.app"

# Essential SPA fallback to prevent 404s on route refresh
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

Environment variable management:

Store environment variables directly in each site’s Netlify dashboard, not in netlify.toml . This includes:

Public configuration like API base URLs can live in netlify.toml for transparency, but anything sensitive should be isolated per site.

Backend CORS Configuration:

To prevent unintended cross-environment API access, I rely on a CORS setup derived by FastAPI’s guide but customized for deploy previews:

from fastapi.middleware.cors import CORSMiddleware 
import os

# Environment-specific allowed origins
allowed_origins = []

if os.getenv("ENVIRONMENT") == "production": 
  allowed_origins = ["https://app.example.com"] 
elif os.getenv("ENVIRONMENT") == "staging": 
  allowed_origins = ["https://staging.example.com"] 
elif os.getenv("ENVIRONMENT") == "preview": 
  # For deploy previews, pattern match 
  allowed_origins = ["https://deploy-preview-*--example.netlify.app"]
  
app.add_middleware( 
  CORSMiddleware, 
  allow_origins=allowed_origins, 
  allow_credentials=True, 
  allow_methods=["GET", "POST", "PUT", "DELETE"], 
  allow_headers=["*"], )

This approach ensures that your staging API cannot be called from production (and vice versa), preventing data leakage and unauthorized access.

Deployment Flow Architecture:

This architecture ensures complete isolation between environments while maintaining clear data flow and access controls.

Common Pitfalls and Solutions:

          [build] 
          base = "packages/frontend"  # Explicit path to app                      
          publish = "packages/frontend/dist"  # Explicit publish directory
# Pattern matching for deploy previews
import re 

allowed_origin_patterns = [ r"https://deploy-preview-\d+--yoursite.netlify.app" ]

def is_allowed_origin(origin): 
  return any(re.match(pattern, origin) for pattern in allowed_origin_patterns)

Maintenance Workflow Benefits:

Once properly implemented, this separation strategy provides significant operational advantages: