| name | doppler |
| description | Doppler CLI workflows for secrets management in this workspace. Covers creating projects non-interactively (agent-friendly), renaming the default envs to development/preview/production, uploading secrets, type-safe env access in TypeScript apps, CI integration via GitHub Actions, and syncing secrets to Cloudflare Workers (wrangler) and Fly.io (flyctl). Use this skill whenever a task involves `doppler run`, `DOPPLER_TOKEN`, creating or renaming Doppler projects/configs, adding `.env` secrets to Doppler, syncing Doppler secrets to a deployment target, or wiring Doppler into a new service or CI workflow.
|
doppler
Doppler is the secret store this workspace uses across every package. All
package.json scripts that need env vars are wrapped in doppler run -c <config> --, and deploys pull secrets out of Doppler into the deployment
target (Cloudflare Workers, Fly, GitHub Actions) instead of committing
.env files.
Shell env var ordering
When a command needs both shell env vars and Doppler secrets, put the shell env vars before doppler run, never after.
CLOUDFLARE_ENV=preview doppler run -c preview -- vite dev
CLOUDFLARE_ENV=preview doppler run -c preview -- vite build
doppler run -c preview -- CLOUDFLARE_ENV=preview vite dev
doppler run -c preview -- CLOUDFLARE_ENV=preview vite build
Why:
- tooling like Wrangler and
@cloudflare/vite-plugin reads some env vars from the outer process setup
- this keeps scripts consistent and easy to scan
- it avoids subtle scope confusion about which process sees which env var
Always run doppler --help and doppler <command> --help first. The
help output is the source of truth. Do not pipe it through head/tail —
read it in full.
Naming convention used in this workspace
All Doppler projects in this workspace use these env slugs:
development — local dev, default for doppler run with no -c
preview — staging / preview worker / preview database
production — live site, workers, and Fly apps
The default slugs Doppler ships with (dev, stg, prd) are always
renamed to the above before using a new project. Scripts everywhere in
this repo rely on -c preview and -c production existing — never leave
a new project on the defaults.
Example from plugin-mcp/package.json:
"dev": "doppler run -c preview -- vite",
"build": "doppler run -c production -- pnpm vite build",
Example from db/package.json:
"diff:prod": "doppler run -c production --command '...'",
"diff:preview": "doppler run -c preview --command '...'",
"diff:dev": "doppler run -c dev --command '...'"
Note the db package still uses the legacy dev slug — new projects
should standardize on development instead.
Authentication for agents (no doppler login)
doppler login is interactive (opens a browser). To run Doppler from an
agent or non-interactive script, skip login entirely:
export DOPPLER_TOKEN="dp.pt.xxxxx"
doppler configure set token="dp.pt.xxxxx" --scope=/
doppler projects list --token "dp.pt.xxxxx"
Token types:
- Personal token (
dp.pt.*) — full workplace access. Generate from
Dashboard → Settings → API → Personal Tokens. Use for local agents.
- Service account token — scoped, better for CI/production.
- Service token (
dp.st.*) — read-only for one config, safest for
deploys.
Orgs/workplaces cannot be created from the CLI. You must sign up via
the dashboard once. After that, everything is CLI-able.
Create a new project + rename envs (bootstrap script)
Use this exact script when bootstrapping a new package or service. It
creates the project, renames the three default envs to our convention,
and renames the matching child configs (otherwise doppler run -c development breaks — see gotcha below).
#!/usr/bin/env bash
set -euo pipefail
PROJECT=$1
doppler projects create "$PROJECT" --description "$PROJECT service"
doppler environments rename dev -p "$PROJECT" --name Development --slug development --yes
doppler environments rename stg -p "$PROJECT" --name Preview --slug preview --yes
doppler environments rename prd -p "$PROJECT" --name Production --slug production --yes
doppler configs update dev -p "$PROJECT" --name development
doppler configs update stg -p "$PROJECT" --name preview
doppler configs update prd -p "$PROJECT" --name production
doppler setup --no-interactive -p "$PROJECT" -c development
Gotcha: env rename ≠ config rename. Each environment contains one
root config that starts with the same slug. Renaming the env leaves the
child config stuck on the old name, which breaks doppler run -c development. Always follow environments rename with configs update.
Add secrets
doppler secrets set API_KEY="abc" DATABASE_URL="postgres://..." -p website -c development
doppler secrets upload .env -p website -c development
doppler secrets upload -p website -c preview \
<(doppler secrets download -p website -c development --no-file --format json)
doppler secrets set silently overwrites existing keys. If you want
to preserve values, check first with doppler secrets get KEY -p ... -c ... --plain before setting.
See env vars available
doppler secrets -p website -c development
doppler secrets get DATABASE_URL -p website -c development --plain
doppler secrets download -p website -c development --no-file --format env
doppler secrets download -p website -c development --no-file --format json
For the currently bound project/config (set via doppler setup) just run
doppler secrets with no flags.
Type-safe env in a TypeScript app
This workspace uses a hand-maintained env.ts object rather than a
validator library. The pattern is:
- Read each var off
process.env into a single env object.
- At module load, throw if any server-side var is missing.
- Import
env everywhere instead of touching process.env directly.
Example (website/src/lib/env.ts):
export const env = {
PUBLIC_URL: process.env.PUBLIC_URL,
DATABASE_URL: process.env.DATABASE_URL,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
GITHUB_APP_PRIVATE_KEY: process.env.GITHUB_APP_PRIVATE_KEY,
}
for (const k in env) {
if (
env[k] == null &&
(typeof window === 'undefined' || k.includes('PUBLIC'))
) {
throw new Error(`Missing env var ${k}`)
}
}
Rules for adding a new env var:
- Add it to Doppler first (
doppler secrets set NEW_VAR=... -p <proj> -c development). Repeat for preview and production.
- Add it to
env.ts.
- Import from
env.ts, never read process.env.NEW_VAR directly in
app code.
PUBLIC_* vars are safe to expose to the browser. Anything else must
only be read server-side.
To discover what the app expects vs what Doppler has:
rg "process\.env\." src/lib/env.ts
doppler secrets -p website -c development
CI: GitHub Actions + Doppler
Every CI job that needs secrets follows this pattern (from
.github/workflows/ci.yml):
jobs:
ci:
runs-on: ubuntu-latest
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with: { node-version: 22 }
- uses: pnpm/action-setup@master
- uses: dopplerhq/cli-action@master
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: pnpm install --frozen-lockfile=false
- run: pnpm test
Two things to wire up:
-
Generate a service account token in the Doppler dashboard
(Settings → Service Accounts). Prefer this over a personal token in
CI so it survives if a team member leaves.
-
Add it to GitHub repo secrets as DOPPLER_TOKEN:
gh secret set DOPPLER_TOKEN --body "dp.sa.xxxxx" --repo owner/repo
Or paste it in GitHub → Settings → Secrets and variables → Actions.
-
Expose it at job level via env: DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN }} so every step (including doppler run
inside package scripts) picks it up automatically.
The dopplerhq/cli-action step just installs the binary. It does not
log in — authentication happens via the DOPPLER_TOKEN env var. If you
forget the env var, doppler run in CI will fail with "no token found".
For workflows that scope the token tighter, pass it directly instead of
relying on job-level env:
- run: doppler run -p website -c production -- pnpm deploy
env:
DOPPLER_TOKEN: ${{ secrets.DOPPLER_TOKEN_PROD }}
Upload secrets to Cloudflare Workers
Cloudflare Workers store their own copy of secrets via wrangler secret put. We sync from Doppler → Wrangler using the --mount flag so
wrangler sees a temporary .env file on disk, then wrangler secret bulk uploads everything at once.
For local development, prefer doppler run directly instead of .env files:
doppler run -c development -- wrangler dev
CLOUDFLARE_ENV=preview doppler run -c preview -- vite dev
Use .env only as a temporary mounted file for wrangler secret bulk, not as the main local-development source of truth.
This is the exact pattern used in plugin-mcp/package.json:
"secrets:preview": "doppler run -c preview --mount .env.preview --mount-format env -- wrangler secret bulk --env preview .env.preview",
"secrets:prod": "doppler run -c production --mount .env.prod --mount-format env -- wrangler secret bulk .env.prod"
Breakdown:
doppler run -c <config> — pulls secrets from the Doppler config.
--mount .env.preview — writes them to a temp file at
.env.preview that only exists for the duration of the command.
Nothing is written permanently to disk.
--mount-format env — serialize as dotenv format (vs json /
yaml).
wrangler secret bulk <file> — wrangler reads the file and PUTs
every key to the worker's secret store in one API call.
--env preview — wrangler env name matches the Doppler config
name, so wrangler.toml's [env.preview] block gets the secrets. The
production script omits --env to target the top-level worker.
Run this once per deploy target whenever secrets change:
pnpm secrets:preview
pnpm secrets:prod
Do not try to use doppler run -- wrangler secret put KEY one var
at a time — wrangler prompts for each value interactively and the
script hangs. Always use wrangler secret bulk with --mount.
Upload secrets to Fly.io
Fly stores secrets via flyctl secrets set or flyctl secrets import.
The import form reads a dotenv-style stream on stdin, which pairs
perfectly with doppler secrets download:
doppler secrets download -p website -c production --no-file --format env \
| flyctl secrets import --app my-fly-app
Or wrap it in a package.json script:
"secrets:fly": "doppler secrets download -p website -c production --no-file --format env | flyctl secrets import --app my-fly-app",
"secrets:fly:preview": "doppler secrets download -p website -c preview --no-file --format env | flyctl secrets import --app my-fly-app-preview"
Gotchas:
flyctl secrets import triggers a restart/deploy of the Fly
machines once all keys are staged. Expect ~30s downtime for
single-machine apps.
--no-file makes doppler secrets download write to stdout
instead of creating a local file. Without it you get doppler.env
sitting on disk which must be gitignored.
- Only keys that changed cause a restart. If all values are
identical to what Fly already has, Fly skips the restart.
- For CI use
--stage to stage without restarting, then flyctl deploy
separately: flyctl secrets import --stage --app my-fly-app.
Quick reference
| Task | Command |
|---|
| Create project | doppler projects create NAME |
| Rename env | doppler environments rename OLDSLUG -p PROJ --name NEW --slug NEW --yes |
| Rename root config | doppler configs update OLDNAME -p PROJ --name NEW |
| Set one secret | doppler secrets set KEY=VAL -p PROJ -c CONFIG |
| Upload from file | doppler secrets upload FILE -p PROJ -c CONFIG |
| List secrets | doppler secrets -p PROJ -c CONFIG |
| Get one value | doppler secrets get KEY -p PROJ -c CONFIG --plain |
| Export env | doppler secrets download -p PROJ -c CONFIG --no-file --format env |
| Bind dir to project | doppler setup --no-interactive -p PROJ -c CONFIG |
| Run with secrets | doppler run -c CONFIG -- COMMAND |
| Debug config source | doppler configure debug |
Docs