Define a Shared Schema¶
An external schema separates what an application expects from where each developer, worktree, or deployment obtains its values.
Start with one explicit reference:
ENV type=enum values=development,staging,production required=true
PORT type=int min=1 max=65535 default=8080 doc="HTTP port"
API_URL type=str format=url schemes=https required=true
Validate the values against the contract:
The schema path is literal and relative to the file containing #@schema. There is no automatic search for a sidecar schema and no environment-variable expansion in the path.
Keep a contract inline¶
External schemas are optional. A small project can keep the same constraints in one file:
#@def ENV type=enum values=development,staging,production required=true
#@def PORT type=int min=1 max=65535 default=8080
#@def API_URL type=str format=url schemes=https required=true
ENV=development
API_URL=https://api.example.com
#@def and schema lines use the same parser and support the same options. Moving a definition does not change its type, default, validation, or secret policy.
Schema grammar¶
A schema accepts only:
- variable definitions in the form
NAME option=value; - blank lines and ordinary comments;
- another
#@schema PATHreference.
It rejects assignments, conditions, #@import, #@mode, and every other composition directive. A definition name follows the normal environment variable syntax: a letter or underscore followed by letters, digits, or underscores.
Types and general options¶
| Option | Purpose |
|---|---|
type= | str, int, float, bool, list, json, or enum |
required=true | Require a value unless an unresolved source is inspected statically |
default= | Supply a value when no assignment exists |
validate= | Apply a regular expression in addition to other constraints |
doc= | Describe the variable in docs and generated templates |
secret=true/false | Force or disable name-based secret classification |
lazy=true | Preserve dependent expansion for fuse |
source= | Resolve a secret through an allowlisted backend |
Defaults pass through the same type conversion and constraints as supplied values. Applicable constraints are cumulative; validate= does not replace a length, range, enum, or URL check.
Document variables¶
Use doc= to keep operational guidance with the contract. Quote descriptions that contain spaces and write \n where the rendered documentation needs a line break:
DATABASE_URL type=str format=url schemes=https required=true doc="PostgreSQL connection URL.\nUse the application role, not an administrator account."
dotenv-fusion docs renders continuation lines with indentation:
DATABASE_URL (str) [required]: PostgreSQL connection URL.
Use the application role, not an administrator account.
dotenv-fusion template prefixes every documentation line with #, so a multiline description cannot introduce an active dotenv assignment. The Python API returns the description with real newline characters through get_docs(). Write \\n when the documentation needs the literal characters \n instead of a line break.
Enum, range, length, and URL constraints¶
ENV type=enum values=development,staging,production
PORT type=int min=1 max=65535
RATE type=float min=0.0 max=1.0
APP_NAME type=str nonempty=true min_length=3 max_length=80
TAGS type=list nonempty=true min_length=1 max_length=10
API_URL type=str format=url schemes=https,wss
values=is required bytype=enumand is invalid for other types. Members are comma-separated, non-empty, and unique.min=andmax=are inclusive and apply only tointandfloat.nonempty=,min_length=, andmax_length=apply only tostrandlist.format=urlrequires a URI scheme and validates syntax locally. It performs no request or DNS lookup.schemes=restrictsformat=urlto a comma-separated list such ashttps,wss.
Invalid option combinations fail with the schema file and line number. Diagnostics never include a value classified as secret or derived from a secret.
Compose schemas¶
Schemas include other schemas with the same explicit directive:
#@schema shared/database.env-schema
#@schema shared/http.env-schema
ENV type=enum values=development,production required=true
References are relative to the schema that declares them. Cycles are rejected with the complete chain.
Every variable may have only one definition when an external schema is used. Duplicates are rejected across nested schemas and between a schema and inline #@def declarations, regardless of load order. Files using only inline definitions retain their historical last-definition-wins behavior.
Share one contract in a monorepo¶
An unprefixed composition import may load a schema:
repository/
├── contracts/
│ ├── shared.env-schema
│ └── payments.env-schema
└── services/
└── payments/
├── .env-fuse
└── contract.env-fuse
#@schema shared.env-schema
PAYMENTS_PORT type=int min=1 max=65535 required=true
PAYMENTS_API_URL type=str format=url schemes=https required=true
#@schema is forbidden inside a conditional block and inside an import using prefix=. The contract graph therefore does not change with runtime conditions or rename definitions implicitly.
With --restrict-imports, schema paths and symlink targets must stay under the main .env-fuse directory, using the same effective boundary as composition imports. Without that opt-in flag, explicit relative, absolute, and symlinked schema locations remain available.
Sources and static inspection¶
A schema may declare a source without resolving it:
Loading the schema graph itself never contacts a backend. Command behavior is explicit:
checkresolves sources by default;check --no-resolve-sourcesvalidates available assignments and the contract without contacting source backends;list,docs, andtemplateinspect sources without resolving them;load,exec, andfusekeep their normal source-resolution behavior.
The dependency-free standalone core can parse schemas and perform static inspection without installing the age sidecar.
AI agent skill¶
The repository includes the skills/dotenv-fusion-schema AI agent skill. It can analyze application code, configuration, manifests, scripts, CI, and documentation across programming languages to propose a reviewable .env-schema.
The skill reports evidence and uncertainty separately from the generated contract. It never reads environment value files, plaintext secrets, age identities, or encrypted bundles, and it never invents defaults, enum members, constraints, or secret source URIs.
Migrate inline definitions¶
Review an extraction without writing anything:
Then create the schema and update .env-fuse:
The migration preserves assignments, imports, comments, other directives, line endings, and the order of extracted definitions. It reparses the contracts before and after extraction and refuses:
- a definition inside a condition;
- a definition or schema contributed by a composition import;
- an existing
#@schema; - a duplicate or a line whose meaning would change in schema syntax;
- an existing schema destination;
- a source or destination symbolic link.
Both outputs are prepared and validated before publication. The new schema is published first without overwriting a destination that appears concurrently; .env-fuse is then replaced atomically. If that final replacement fails, the original .env-fuse remains intact and the error identifies the already published orphan schema.
This migration changes only the location of the contract. It does not encrypt values. To extract selected values from a basic .env into an age bundle, use migrate secret.