Payload Plugins
Pluginspayload-dev-tools

Environments

Boot the site against staging without editing .env.local, and see at a glance what the running process is actually connected to.

For AI / LLMs: View Markdown

Two things go wrong with environments in a Payload app. You edit .env.local to point at staging, forget, and spend an afternoon debugging seeded data that isn't there. Or worse: you don't forget, and pnpm seed wipes a real database.

This plugin answers both — a wrapper that picks the environment at boot, and a panel that tells you which one you're in.

Switching: payload-dev-env

Environment is read once, when the process boots. So switching can't be a toolbar toggle — it's a wrapper around the command, and changing it means a restart. The CLI ships with the plugin:

payload-dev-env staging -- pnpm dev
payload-dev-env staging -- pnpm payload migrate:status
payload-dev-env prod --file ../secrets/.env.prod -- pnpm build

It looks for ./.env.<name>.local, then ./.env.<name> (or the path you pass with --file / PAYLOAD_DEV_ENV_FILE), loads it, and runs your command with those values in front. Nothing is copied or overwritten — .env.local stays exactly as it is, and if no file matches the command doesn't run at all. A wrong environment should be loud, not silent.

Wire the ones you use into package.json:

package.json
{
  "scripts": {
    "dev": "next dev",
    "dev:staging": "payload-dev-env staging -- next dev"
  }
}

Next's own env loading doesn't undo this: @next/env only fills variables that aren't already defined, so what the wrapper injects survives .env.local. (Node forbids --env-file inside NODE_OPTIONS, which is why this is a wrapper and not a flag.)

Knowing where you are

The wrapper stamps PAYLOAD_DEV_ENV and PAYLOAD_DEV_ENV_FILE into the process, so the toolbar's Info view and the /dev overview can name the environment you booted:

development · node v22.x · staging from .env.staging
DATABASE_URI → cluster0.abc.mongodb.net · remote

Alongside it, a present/missing checklist for the variables the installed plugins actually read — PAYLOAD_SECRET, the database URI, ENABLE_SEED if payload-seed is installed, MUX_TOKEN_ID and MUX_TOKEN_SECRET if payload-mux is, and so on. Required-but-missing is called out; optional and unset stays quiet.

Presence only, never values. The snapshot reports set: true and the database host — with credentials and path stripped — and nothing else. A connection string carries a password, and this is data the toolbar fetches over HTTP.

Two warnings are worth the interruption, and appear in red:

  • A development boot pointed at a remote database. The most expensive mistake in the list — it's how a seed run ends up on staging data.
  • A required variable missing for a plugin you installed.

In the snapshot

Everything above is in GET /api/dev under env, so CI and agents can assert on it:

curl -s localhost:3000/api/dev | jq '.env.warnings, .env.database.local'
"env": {
  "nodeEnv": "development",
  "nodeVersion": "v22.x",
  "name": "staging",
  "file": ".env.staging",
  "database": { "variable": "DATABASE_URI", "host": "cluster0.abc.mongodb.net", "local": false },
  "vars": [{ "name": "PAYLOAD_SECRET", "owner": "payload", "required": true, "set": true }/* … */],
  "warnings": ["DATABASE_URI points at cluster0.abc.mongodb.net — this dev boot is writing to a non-local database."]
}

On this page