Q32 of 42 · Playwright
How do you handle environment-specific config and secret management in Playwright?
Short answer
Short answer: Single `playwright.config.ts` driven by env vars (`process.env.BASE_URL`, etc.). Per-environment values come from CI variables, not committed files. Use projects for env-specific test selection. Secrets stay in CI vault; local development uses `.env` files (gitignored) loaded via `dotenv`.
Detail
The clean pattern: one config file, env vars drive the per-environment differences.
// playwright.config.ts
import 'dotenv/config'; // load .env locally
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
extraHTTPHeaders: {
'X-API-Key': process.env.API_KEY ?? '',
},
},
projects: [
{ name: 'chromium', use: { /* ... */ } },
],
});
Where values come from:
- Local dev: a
.envfile (gitignored) loaded bydotenvat the top of the config. - CI: CI provider's secret store. GitHub Actions
secrets.PROD_API_KEY, CircleCI context, Buildkite cluster secrets. - Per-env CI workflows: separate workflows for staging vs prod, each with its own secret bindings.
Per-environment test selection via projects:
projects: [
{ name: 'staging-smoke', use: { baseURL: process.env.STAGING_URL }, testMatch: /\.smoke\./ },
{ name: 'prod-readonly', use: { baseURL: process.env.PROD_URL }, testMatch: /\.readonly\./ },
]
For multi-role auth secrets: store credentials per role in CI vars (CYPRESS_ADMIN_PASSWORD, CYPRESS_VIEWER_PASSWORD). The setup project uses them; tests never see them directly.
Hard rules:
- Never commit
.envfiles. Add to.gitignore. - Never log secrets.
console.log(process.env)in CI accidentally leaks them. Test code should never print env vars. - Don't hardcode
baseURLper environment in different config files. One config + env vars is cleaner. - Production E2E is its own conversation. Most teams don't run write-side tests against prod; if you do, dedicated test tenant with explicit data isolation is required.
For local dev convenience, ship a .env.example (committed) with placeholder names so newcomers know what to set:
# .env.example
BASE_URL=http://localhost:3000
ADMIN_EMAIL=admin@x.com
ADMIN_PASSWORD=
The senior signal: knowing the boundary (CI vars for secrets, .env for local convenience, never committed), per-env test selection via projects, and the production-testing caveat.