Skip to Content
Env ValidationConfiguration

Configuration

Configure the env key in miso.json as an array of entries — one per app or .env file. Each entry defines its own path and validation rules, so variables are always checked against the file they belong to.

>miso.json
{ "env": [ { "scope": "web", "label": "web", "path": "apps/web/.env.local", "required": "all", "variables": { "PORT": "port", "DATABASE_URL": "url", "NODE_ENV": { "type": "enum", "values": ["development", "production", "test"], "optional": true } } }, { "scope": "api", "label": "api", "path": "apps/api/.env", "required": "all", "variables": { "REDIS_URL": { "type": "url", "schemes": ["redis", "rediss"] } } } ] }

Miso validates every entry and reports all the failures together, each grouped under its label — you see the whole list in one pass rather than fixing one, rerunning, and hitting the next.

scope

Which target this entry applies to: a workspace member name, a configured task/script name, or the reserved value "global".

Required on root miso.json entries. miso env rejects any root entry with no scope:

env config invalid — every root entry needs a scope (a target name or "global") .env.local

Member miso.json files (nested at <member>/miso.json) don’t set scope at all — they scope by file location. Every entry inside a member’s own config applies only to that member.

One scope, one file

A scope belongs to the root config or to the member’s own config, never both. Declaring a root entry scoped to a member that also declares its own env fails:

env scope declared in two places — keep it in the root config or the member's, not both web — root miso.json and apps/web/miso.json

Pick whichever fits: root when you want every schema in one place, the member’s config when the app owns its own. Splitting one scope across both is how the two copies drift apart — a variable added to one and not the other goes unvalidated.

Resolution order

When miso resolves the env for a run target T, it layers, later winning on key conflict:

  1. Root entries scoped to "global"
  2. Either root entries scoped to T (or, for a member target, to the member’s directory basename), or — when T is a member declaring its own env — that member’s <member>/miso.json entries

The merged result is gap-filled under the ambient shell environment — a variable already set in your shell is never overridden, at any layer.

"global" is reserved: it can’t be used as a workspace, task, or script name, so it never collides with a real target.

Validation vs. injection

scope is enforced differently depending on what’s using it:

  • Validation (miso env, --env) is strict — every root entry must declare a scope, or the command fails.
  • Injection (loading .env values into a running script) is permissive — an entry whose scope doesn’t match the current target is silently skipped, no error. This lets a root miso.json carry entries for members that aren’t relevant to the command you’re currently running.

scope itself is a miso-mode requirement: a delegated repo (repo: "turbo" or "nx") resolves targets through the delegate, so root entries there don’t need one and "global" carries no reserved meaning. Validation still covers the whole repo in every mode — see Delegated repos.

Delegated repos

In a delegated repo, ownership splits by job:

  • Validation always traverses the whole repo. miso env and --env check every workspace member’s miso.json schema alongside the root’s, in every repo mode. A clean exit means the whole repo’s declared variables are present — that’s the point of a preflight gate.
  • Injection follows orchestration, not repo mode. When turbo runs the task, turbo owns its env and miso injects nothing. When a repo.tasks.<name> override hands the task to miso, miso spawns the processes and injects the resolved env for each one — the same as miso mode.

label

An optional name for this entry, used in log output and error messages. If omitted, Miso falls back to the entry’s path value.

Using a label is recommended in any project with more than one env file — it makes errors immediately actionable. Failures print grouped under the label, one variable per line:

api REDIS_URL: missing required variable

path

The path to the .env file for this entry, relative to the project root.

If omitted, Miso falls back to the same discovery order used when no env config is present at all:

  1. .env.local
  2. .env.production
  3. .env.development
  4. .env

override

Path to this scope’s override file, relative to the project root (or the member’s directory for a member-local entry). Used by miso env -g -o: its values are layered on top of the baseline values (populated by -p when given), winning on key conflict. One override file per scope — at the root level for "global", or one per member entry.

See Env generation for the full --generate workflow.

required

Controls which variables in this entry must be present:

"all"

Default

Every variable must be present unless marked optional: true on that variable.

"none"

No variables are required — only type-check those that exist.

string[]

Only the listed keys are required; others may be absent.

variables

Defines the variables to validate for this entry. Accepts either an object (type validation) or an array (presence only).

Object form — each key is a variable name, each value is a type shorthand string or full VarConfig:

>miso.json
{ "variables": { "PORT": "port", "DATABASE_URL": "url", "APP_NAME": { "type": "string", "optional": true } } }

Array form — only checks that the listed variables exist, no type validation:

>miso.json
{ "variables": ["DATABASE_URL", "API_KEY", "REDIS_URL"] }

Use the array form when you only care about presence (e.g. in CI) and don’t need format validation.

Single-app projects

If you only have one .env file, you still use an array — just with a single entry:

>miso.json
{ "env": [ { "scope": "global", "path": ".env.local", "variables": { "PORT": "port", "DATABASE_URL": "url" } } ] }
Last updated on