Skip to content

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-fuse
#@schema application.env-schema

ENV=development
PORT=8080
API_URL=https://api.example.com
application.env-schema
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:

dotenv-fusion check -f .env-fuse

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:

.env-fuse
#@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 PATH reference.

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 by type=enum and is invalid for other types. Members are comma-separated, non-empty, and unique.
  • min= and max= are inclusive and apply only to int and float.
  • nonempty=, min_length=, and max_length= apply only to str and list.
  • format=url requires a URI scheme and validates syntax locally. It performs no request or DNS lookup.
  • schemes= restricts format=url to a comma-separated list such as https,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:

contracts/application.env-schema
#@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
services/payments/.env-fuse
#@import contract.env-fuse
ENV=development
PAYMENTS_PORT=8080
services/payments/contract.env-fuse
#@schema ../../contracts/payments.env-schema
contracts/payments.env-schema
#@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:

API_TOKEN required=true source="age://store/application.env.age#TOKEN"

Loading the schema graph itself never contacts a backend. Command behavior is explicit:

  • check resolves sources by default;
  • check --no-resolve-sources validates available assignments and the contract without contacting source backends;
  • list, docs, and template inspect sources without resolving them;
  • load, exec, and fuse keep 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:

dotenv-fusion migrate schema \
  -f .env-fuse \
  -o application.env-schema \
  --dry-run

Then create the schema and update .env-fuse:

dotenv-fusion migrate schema \
  -f .env-fuse \
  -o application.env-schema

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.