TL;DR
We support 1Password Environments local .env files for managing local development secrets. Each backend service auto-detects a 1Password-mounted .env at a conventional path and loads from it; if it's not present, the service falls back to the standard committed .env.* files. No env vars to set, no scripts to change, and zero impact on Docker / CI / staging / prod or contributors who don't use 1Password.
You only need to do this once per machine, per service you actually run.
Why
Before this change, devs sharing secrets through a personal .env had two bad options: paste plaintext credentials into apps/<svc>/src/.env (risk of leak, drift between teammates), or wire up a custom shell-script flow per service. With 1Password Environments, we get:
- Secrets live in 1Password, never on disk in plaintext.
- One source of truth for the team's local dev credentials.
- Rotations propagate automatically — no "hey, the staging key changed" Slack threads.
- Auditable access via 1Password.
Prerequisites
- 1Password 8 desktop app on macOS or Linux. Windows is not supported by 1Password for this feature.
- Membership in the team 1Password account, with access to the dev
Environment(ask the team lead if you can't see it). - A clean checkout of
novuonnext.
Setup
1. Find the Environment in 1Password
Open the 1Password desktop app → Developer sidebar → Environments → select the Novu dev Environment.
If you don't see Environments, make sure you're on a recent 1Password 8 build and that Settings → Developer → Show Environments is enabled.
2. Mount each service's .env at the conventional path
For each service you need to run locally, configure a Local .env file destination at the path below. Mount paths are outside src/ on purpose — see How it works for why.
Service | Mount path (absolute) | When you need it |
api | <repo>/apps/api/.env | Always |
worker | <repo>/apps/worker/.env | When testing notification triggering |
ws | <repo>/apps/ws/.env | When working on real-time / inbox features |
inbound-mail | <repo>/apps/inbound-mail/.env | When working on inbound email parsing |
In 1Password, for each Environment destination:
- Click Configure destination under Local
.envfile. - Click Choose file path, navigate to the path in the table, name the file
.env. - Click Mount .env file.
Do not mount inside apps/<svc>/src/. nest-cli watches that tree and treats .env as an asset to copy into dist/. The asset copier doesn't handle named pipes (FIFOs), and the file watcher will fire restart loops on every FIFO open/close — both documented gotchas in the 1Password docs.
3. Delete any stale FIFOs in src/
If you previously mounted at apps/<svc>/src/.env, remove the dead FIFO:
rm apps/api/src/.env apps/worker/src/.env apps/ws/src/.env apps/inbound-mail/src/.env 2>/dev/null(Errors for paths that don't exist are fine.)
4. Verify
From the repo root:
cat apps/api/.envFirst time per session, 1Password pops up an authorization prompt. Approve it — you should see your env vars. If cat works, the API will work.
Then start the service:
pnpm start:api:devYou'll see one more 1Password authorization prompt (the dev server reading the FIFO at boot). After that, the dev server is running with secrets loaded.
How it works
Each service's apps/<svc>/src/config/env.config.ts looks for an override at apps/<svc>/.env before the standard load path:
const localOverridePath = path.join(__dirname, '..', '..', '.env');
const defaultPath = path.join(__dirname, '..', getEnvFileNameForNodeEnv(process.env.NODE_ENV));
dotenv.config({
path: fs.existsSync(localOverridePath) ? localOverridePath : defaultPath,
});Key properties:
fs.existsSynconlystats the inode — it does not open or read the FIFO, so it doesn't burn the one-shot pipe.- The override path is one directory above
src/, outside everything nest-cli watches or copies. **/.envis already in.gitignore, so the mount won't be accidentally committed.- In Docker, CI, staging, and prod the file simply doesn't exist, so the loader takes the unchanged default branch (compiled
dist/.env.productionetc.).
Behavior matrix
Environment | apps/<svc>/.env exists? | Loader uses |
Docker image | no | dist/.env.production |
Staging / prod runtime | no | real process.env (k8s / secrets manager) |
CI | no | dist/.env.test etc. |
Local non-1P contributor | no | dist/.env (existing flow) |
Local 1P dev | yes (FIFO) | the FIFO, one read per process start |
Troubleshooting
"Missing env vars" / app crashes on boot
Most common cause: the FIFO is mounted inside src/. Move it to apps/<svc>/.env (one directory up) and delete the old FIFO.
Verify with:
stat -f "%N: type=%HT mode=%Sp" apps/api/.envExpected output for a 1P mount:
apps/api/.env: type=Fifo mode=prw-------1Password never prompts when I run pnpm start
Usually means the file in your IDE is holding the FIFO open. Per 1Password's docs, local .env mounts aren't designed for concurrent access — if Cursor / VS Code / JetBrains has the file open in an editor tab, it can lock out the dev server.
Fix: close the file in the editor. Optional: add the path to .cursorignore so the IDE doesn't index it.
Watch mode keeps restarting
FIFOs emit filesystem events on every open/close, which some watchers misinterpret as file modifications (the same issue the 1Password docs flag for Vite). If you mounted outside src/ per the table above, nest-cli won't watch the file and you should not see this. If you do, double-check the mount path.
I rotated a secret in 1Password but the app still has the old value
The FIFO is read once at process startup. Restart the dev server (Ctrl+C, pnpm start:api:dev) to pick up the new value.
I'm offline
1Password serves the most recent contents synced to your device, so previously-authorized values keep working. New values you push from elsewhere won't appear until you're back online.
Non-1P contributors
Nothing changes for you. Keep using apps/api/src/.env (or the pnpm setup flow) as before. The override branch is opt-in by file presence — if you don't mount anything at apps/<svc>/.env, the default load path is identical to what it was before.
Open questions / TODO
dashboard (it has its own .env setup that doesn't go through getEnvFileNameForNodeEnv).apps/<svc>/.env to .cursorignore defaults?References
- 1Password — Access secrets through local
.envfiles (beta) - 1Password — Environments overview
- Internal PR introducing the override: (link once merged)
- Slack channel: (link to #eng-tooling or equivalent)