# Placeholders and .env (/docs/placeholders)

## Syntax [#syntax]

A string value can refer to an environment variable:

| Placeholder       | Value                                                                     |
| ----------------- | ------------------------------------------------------------------------- |
| `${VAR}`          | The value of `VAR`. A problem when `VAR` is not set                       |
| `${VAR:-default}` | The value of `VAR`, or `default` when `VAR` is unset or empty             |
| `${VAR:?message}` | The value of `VAR`. A problem with `message` when `VAR` is unset or empty |
| `$${`             | A literal `${`                                                            |

Placeholders are resolved in values only, never in keys.

```yaml title="config/app.config.yaml"
http:
  port: ${PORT:-3000}
database:
  url: ${DATABASE_URL}
  password: ${DATABASE_PASSWORD:?Set DATABASE_PASSWORD in .env.local}
```

In TOML, JSONC and JSON5 a placeholder has to be a string, so quote it:

```toml title="config/app.config.toml"
[server]
port = "${PORT:-3000}"
```

## Whole values are typed [#whole-values-are-typed]

A value that is exactly one placeholder has the same type for tsc and at run time:

- **In the base file**, it has the type of its default. With `PORT=8080`, `port: ${PORT:-3000}` is the number `8080`. Without a
  default (`${PORT}` or `${PORT:?message}`), it is a string, so `port: ${PORT}` is `'8080'`.
- **In an environment or local file**, it has the type of the base key it overrides. If `app.config.yaml` has `port: 3000`,
  `port: ${PORT}` in `app.production.config.yaml` is the number `8080`.

The variable is read with the file's own rules for that type (`true` is a boolean in YAML and TOML), and a value that doesn't
read as that type is a problem. A placeholder inside a longer string stays a string: `url: http://localhost:${PORT}` is
`'http://localhost:8080'`.

| In the base file                | Type      | With `PORT=8080`, `DEBUG=true`, `NAME=api` |
| ------------------------------- | --------- | ------------------------------------------ |
| `port: ${PORT:-3000}`           | `number`  | `8080`                                     |
| `debug: ${DEBUG:-false}`        | `boolean` | `true`                                     |
| `port: ${PORT}`                 | `string`  | `'8080'`                                   |
| `name: ${NAME:?required}`       | `string`  | `'api'`                                    |
| `url: http://localhost:${PORT}` | `string`  | `'http://localhost:8080'`                  |

To get a number from a placeholder with no default, give it a default or validate it with a schema that coerces
(`z.coerce.number()`, see [Schemas](/docs/schemas)).

## .env files [#env-files]

Placeholders read from these files, each later one winning over the ones before it:

1. `.env`
2. `.env.local`
3. `.env.<environment>`
4. `.env.<environment>.local`

The real environment variables win over all of them. Each file is looked for next to the base config file first, then in the
project root (the nearest folder with a `package.json`). `.env` files are parsed with Node's `util.parseEnv`.

tyfigo never writes `process.env`. `.env` files only supply values to placeholders and to [`loadEnv`](/docs/schemas#loadenv); they are never imported as modules.

<Callout title="Bundled servers">
	A server bundle built with a bundler plugin reads `.env` files from the working directory when it starts, not from the
	machine that built it. It picks its layers and `.env.<env>` files by `NODE_ENV` when it starts, unless the plugin's
	`environment` option fixed them. See [Server bundles](/docs/browser-builds#server-bundles), and
	[Browser builds](/docs/browser-builds) for what client bundles do.
</Callout>

## Problems [#problems]

A missing variable stops the import with every problem listed:

```txt
Invalid config /srv/app/config/app.config.yaml
/srv/app/config/app.config.yaml:5:8 - Environment variable 'DATABASE_URL' is not set (missing-variable)
```

A value that doesn't read as the placeholder's type is a `placeholder-type` problem, at the placeholder:

```txt
Invalid config /srv/app/config/app.config.yaml
/srv/app/config/app.production.config.yaml:2:9 - Expected number at 'http.port', but environment variable 'PORT' is "abc" (placeholder-type)
```

A malformed placeholder such as `${1PORT}` or an unclosed `${PORT` is an `invalid-placeholder` problem, reported by tsc as
well as at run time.
