---
title: "@env-spec Reference"
description: "Reference docs and details for @env-spec"
---

:::tip
In this spec, we don't make any assumptions about the meaning of specific decorators, or function calls. This document just deals with how the syntax itself is parsed and structured.
:::

## Config Items
Config items define individual env vars.

- Each has a key, an optional value, and optional attached comments
- Keys must start with `[a-ZA-Z_]`, followed by any of `[a-ZA-Z0-9_]` -- ✅ `SOME_ITEM`, ❌ `BAD-KEY`, ❌ `2BAD_KEY`
- Setting no value is allowed and will be treated as `undefined` -- `UNDEF_VAR=`
  - note that no value (`ITEM=`) is treated slightly differently than an explicit `ITEM=undefined` when combining multiple definitions
- An explicit empty string is allowed -- `EMPTY_STRING_VAR=""`
- Single-line values may be wrapped in quotes or not, and will follow the common value-handling rules (see below)
- Multi-line string values may be wrapped in either ``( ' | " | """ | ``` )`` - but we **strongly** recommend using triple backticks only for consistency

````env-spec
NO_VALUE=
EXPLICIT_UNDEFINED=undefined
EMPTY_STRING=""
UNQUOTED=asdf
QUOTED="asdf"
FUNCTION_CALL=fn(foo, "bar")
MULTILINE_STRING=```
multiple
lines
```
````

## Comments and @decorators
Comments in env-spec (like traditional .env files) start with a `#`. Unlike traditional .env files, comments may contain additional metadata by using `@decorators`, which may be attached to specific config items, sections, or the entire document.

- Comments can be either on their own line, or at the end of a line after something else
- Leading whitespace after the `#` is optional, but a single space is recommended
- If a comment line starts with a @decorator, it will be considered a _decorator comment line_
- Otherwise it is a _regular comment line_ and any contained @decorators will be ignored
- A decorator comment line may contain multiple decorators
- A decorator comment line may end with an additional comment, in which decorators will be ignored
- A post-value comment may also contain decorators, but is not recommended


```env-spec
   # ❌ leading space makes this invalid
# this is a regular comment line
# @dec2 @dec2=foo # this is a decorator comment line
FOO=val # this is a post-value comment
BAR=val # @dec # post-value comments may also contain decorators

# regular comment lines @ignore contained @decorators
# @dec # as are @decorators within an extra comment after a decorator comment line
BAZ= # @dec # this is @ignored too
```

### Decorators

Decorators are used within comments to attach structured data to specific config items, or within a standalone comment block to alter a group of items or the entire document and loading process.

- Each decorator has a name and optional value (`@name=value`) or is a bare function call `@func()`
- The two forms carry a meaning: **bare function calls `@func(...)` may be used multiple times** (their args accumulate, e.g. multiple `@docs(...)` links), while **decorators with a value `@name=value` are single-use**
- A decorator value may itself be a function call (`@name=func(args)`), an object literal (`@name={key=value}`), or an array literal (`@name=[a, b, c]`). These are all single-use values, not bare function calls
- Using the name only is equivalent to setting the value to true -- `@required` === `@required=true`
- Multiple decorators may be specified on the same line
- Decorator values will be parsed using the common value-handling rules (see below)


```env-spec title=".env.schema"
# @willBeTrue @willBeFalse=false @explicitTrue=true @undef=undefined @trueString="true"
# @int=123 @float=123.456 @willBeString=123.456.789
# @doubleQuoted="with spaces" @singleQuote='hi' @backTickQuote=`hi`
# @unquoted=this-works-too @withNewline="new\nline"
# @funcCallNoArgs=func() @dec=funcCallArray(val1, "val2") @dec=funcCallObj(k1=v1, k2="v2")
# @objectLiteral={k1=v1, k2="v2"} @arrayLiteral=[a, b, c]  # standalone object/array values
# @anotherOne # and some comments, this @decorator is ignored
# this is a comment and this @decorator is ignored
```



### Dividers
A divider is a comment that serves as a separator, like a `<hr/>` in HTML.

- A comment starting with `---` or `===` is considered a divider -- `# ---`, `# ===`
- A _single_ leading whitespace is optional (but recommended -- `# ---`, `#---` )
- Anything after that is ignored and valid -- `# --- some info`, `# ------------`


```env-spec
# a divider can be used to visually separate sections
# ---
ITEM1=
ITEM2=
# --- another divider ---
ITEM3=
```



### Config Item Comments
Comment lines directly preceding an item will be attached to that item, along with the decorators contained within.

- A blank line or a divider will break the above comments from being attached to the item below
- Both decorator and regular comment lines may be interspersed
- Standalone comments only count as decorator comments when the comment content starts with `@`
- Post-value comments may also contain decorators, but should be used sparingly

```env-spec
# these comments are attached to ITEM1 below
# @dec1 @dec2 # meaning these decorators will affect the item
# additional comments can be interspersed with decorators
ITEM1= # @dec3 # and a post-value comment can be used too

# not attached due to blank line

# also not attached due to divider
# ---
ITEM2=
```


### Comment blocks & document header

A comment block is a group of continuous comments that is not attached to a specific config item.

- The comment block is ended by an empty line, a divider, or the end of the file
- All comment blocks before the first config item in a document are considered part of the same _document header_ until one of those endings appears
- Decorators from this header can be used to configure all contained elements, or the loading process itself
- We recommend ending the header with a divider for clarity, but `# ---` is optional

```env-spec
# this is the document header and usually contains root decorators
# which affect default settings and the behavior of the tool that will be parsing this file
# @dec1 @dec2
# ---

# this is another comment block
# and is not attached to an item

# this comment is attached to the item below
ITEM1=



```

## Common rules

### Value handling

Values are interpreted similarly for config item values, decorator values, and values within function call arguments. Values may be wrapped in quotes or not, but handling varies slightly:

#### Unquoted values
- Will coerce `true`, `false`, `undefined` -- `@foo=false`
- Will coerce numeric values -- `@int=123 @float=123.456`
  - if the number is too large, would lose precision, or would change formatting, it will remain a string
- May be interpreted as a function call (see below)
- Otherwise will be treated as a string
- May not contain other characters depending on the context:
  - config item values - may not contain `#`
  - decorator values - may not contain `[ #]`
  - function call arg values - may not contain `[),]`

#### Quoted values
- A value in quotes is _always_ treated as a string -- `@d1="with spaces"`, `@trueString="true"`, `@numStr="123"`
- All quote styles ``[`'"]`` are ok -- ``@dq="c" @bt=`b` @sq='a'``
- Escaped quotes matching the wrapping quote style are ok -- `@ok="escaped\"quote"`
- Single quote wrapped strings do not support [expansion (see below)](#expansion)
- In `"` or `` ` `` wrapped values, the string `\n` will be converted to an actual newline
- Multi-line strings may be wrapped in ``(```|"""|"|')``
  - only available for config item values, not decorators or within function args


### Regex literals

Regex literals use JavaScript-style `/pattern/flags` syntax and can be used anywhere a value is expected, for example as function arguments or decorator option values.

- The pattern is delimited by `/` characters
- Forward slashes within the pattern must be escaped as `\/`
- Optional flags (`g`, `i`, `m`, `s`, `u`, `y`) may follow the closing `/`

```env-spec
ITEM=fn(/^dev.*/i, dev, "production", prod)
# @type=string(matches=/^sk-[a-zA-Z0-9]+$/)
API_KEY=
```

### Function calls

Function calls may be used for item values `ITEM=fn()`, decorator values `# @dec=fn()`, and bare decorator functions `# @func()`.
In each case, much of the handling is the same.

- a value must not be wrapped in quotes to be interpreted as a function call
- function names must start with a letter, and can then contain letters, numbers, and underscores `/[a-ZA-Z][a-ZA-Z0-9_]*/`
- you can pass no args, a single arg, or multiple args
- you may also pass key value pairs at the end of the list
- each value will be interpreted using common value-handling rules (see above)

```env-spec
NO_ARGS=fn()
SINGLE_ARG=fn(asdf)
MULTIPLE_ARGS=fn(one, "two", three, 123.456)
KEY_VALUE_ARGS=fn(key1=v1, key2="v2", key3=true)
MIXED_ARGS=fn(item1, item2, key1=v1, key2="v2", key3=true)
NOT_FN_CALL="fn()" # treated as string
```

### Object & array literals

Standalone objects and arrays use distinct brackets, keeping them unambiguous from function calls (which use `()`):

- `{ key=value, ... }` is an **object**, where keys use `=` (as elsewhere in the spec), not `:`
- `[ value, ... ]` is an **array**
- they may be nested, and used as decorator values or as function-call arguments
- they are parsed using the common value-handling rules, and may span multiple lines (see [Multi-line literals](#multi-line-literals) below)
- they are **not yet supported as standalone config item values** (e.g. `ITEM={...}`). For now an item value that starts with `{`/`[` is still treated as a string

```env-spec
# @dec={ key1=v1, key2="v2", nested={ a=1 } }
# @dec=[a, b, c]
# @dec=fn(opts={ retries=3 }, tags=[x, y])
ITEM=
```

This is why per-item options are written `@sensitive={preventLeaks=false}` rather than `@sensitive(...)`. The latter (a bare function call) is reserved for repeatable decorators.

#### When to reach for a literal (and when not to)

Object/array literals earn their place only where there is **no function-call `()` already carrying the structure**. In practice that means two situations:

1. **The value of a non-function decorator**, e.g. `@sensitive={preventLeaks=false}`. These decorators can't take bare `()` args (that syntax is reserved for repeatable decorators), so an object value is the only way to attach options.
2. **A single argument that is itself a nested object or list**, sitting alongside other args, e.g. `fn(retry={count=3, backoff=2})`.

Do **not** wrap a function's (or function-decorator's) own named arguments in a literal. The parentheses already _are_ the options bag. The following are correct as written, and the literal-wrapped versions are redundant:

| Prefer | Avoid |
| --- | --- |
| `@type=string(minLength=3, maxLength=50)` | `@type=string({minLength=3, maxLength=50})` |
| `@generateTsTypes(path=env.d.ts)` | `@generateTsTypes({path=env.d.ts})` |
| `cache(fn, ttl=1h, key=k)` | `cache(fn, {ttl=1h, key=k})` |

Also prefer the existing positional/comma forms for:

- **ordered pairs**: `ifs(cond1, val1, cond2, val2, default)` and `remap($VAR, match1, result1, ...)` read like a spreadsheet `IFS()`/lookup and stay flat. Crucially, `remap` match keys may be **regexes or non-identifier strings**, which cannot be object keys (keys must match `/[a-zA-Z][a-zA-Z0-9_]*/`), so an object would be strictly less expressive.
- **value lists**: `@type=enum(dev, staging, prod)` already reads as a list; `enum([...])` only adds brackets.

#### Multi-line literals

Object and array literals may span multiple lines, which is handy for long key lists
(e.g. picking many keys to import). The continuation convention matches function calls
and depends on where the literal lives:

- **Inside a decorator** (which lives in a `#` comment), every continuation line must
  start with `#`:

  ```env-spec
  # @import(./.env.shared, pick=[
  #   DATABASE_URL,
  #   REDIS_URL,
  #   STRIPE_KEY,
  # ])
  DATABASE_URL=
  ```

  ```env-spec
  # @sensitive={
  #   preventLeaks=false,
  # }
  SECRET=
  ```

- **On an item value line**, where the literal is nested in a function call, plain
  newlines are used (no `#`):

  ```env-spec
  ALLOWED=fn([
    dev,
    staging,
    prod,
  ])
  ```

A trailing comma is allowed. If a `#`-prefixed continuation is omitted in a decorator,
the bracket simply falls back to a plain string value and the following lines remain
independent config items. They are never silently absorbed into the literal.

Within a multi-line literal (or function call), `#` introduces a comment that runs to the
end of the line, so entries can be commented out or annotated. This applies the same way
to multi-line function-call arguments.

```env-spec
# @import(./.env.shared, pick=[
#   DATABASE_URL,
#   # REDIS_URL,        ← commented out, skipped
#   STRIPE_KEY, # primary payment key
# ])
DATABASE_URL=
```

### String expansion ||expansion||
While the parser itself does not include any implemention of specific functions, it does handle _expansion_ of strings - and it uses several function calls under the hood to do so. This means a few basic function calls, while not implemented, have specific inherent meaning and must be implemented similarly across all tools that support this spec.

Expansion can be used within item values, decorator values, and function call arguments.

_Note that single quote wrapped strings are NOT expanded._

- `$ITEM_NAME` -> `ref(ITEM_NAME)`
- `${ITEM_NAME}` -> `ref(ITEM_NAME)`
- `pre${ITEM_NAME}post` -> `concat("pre", ref(ITEM_NAME), "post")`
- `${ITEM_NAME:-defaultval}` -> `fallback(ref(ITEM_NAME), "defaultval")`
- `${ITEM_NAME-defaultval}` -> `fallback(ref(ITEM_NAME), "defaultval")`
- `$(my-cli arg --arg2)` -> `exec("my-cli arg --arg2")`

:::note[Keep it simple]
We recommend using expansion only for simple refs `$ITEM`/`${ITEM}` and skipping the rest.

- Use bracketed version within a larger string - `fn("${ENV}_db")`
- Skip the brackets otherwise - `fn($ENV)`

The rest is implemented to match other popular tools, but we do not recommend using them, as intent can be more clearly expressed using function calls directly.
:::
