# Configuration & Environment

> Read typed, validated configuration from environment variables and .env files, and stop at boot with every problem listed.

This guide covers configuration in Tinka. You will learn about:

- Environment variables and the `.env` file
- The `APP_KEY` and how to generate it
- Reading variables into your App at boot
- Variable interpolation within `.env` files
- Reading values from files, such as Docker secrets
- Environment-specific `.env` files for development, testing and production
- Validating environment variables with typed getters
- Writing `.env` files from a Command

## Overview

An App reads the settings that change between machines, such as its port, its URL and its API keys, from environment variables, so the same binary runs on your laptop, in tests and in production. The `env` package (`github.com/mucolabs/tinka/env`) reads the real environment and your `.env` files, converts each variable to the Go type your App needs, and collects every missing or invalid one. A misconfigured App stops at boot with the whole list of problems, instead of failing later on the first one it meets.

## Environment variables

Environment variables hold secrets and settings that differ between environments. During development you keep them in a `.env` file, one `KEY=value` per line. In production you set them in your hosting platform's settings or your container definition.

```dotenv title=".env"
HOST=localhost
PORT=3333
APP_URL=http://localhost:3333
APP_KEY=dev_only_key_1234567890
MAIL_FROM=hello@example.com
STRIPE_KEY=sk_test_1234567890
```

The format is the common dotenv format, so a `.env` file shared with other tools means the same thing in all of them:

- lines starting with `#` are comments, and so is anything after `#` in an unquoted value
- spaces around the key and the value are ignored, and an `export ` prefix is allowed
- values in `'…'`, `"…"` or `` `…` `` keep their spaces and `#` characters and can span several lines, which suits certificates; inside `"…"`, `\n` becomes a newline
- a key starts with a letter or `_`, followed by letters, digits, `_`, `.` or `-`

Tinka never guesses what a broken line means. It reports the file and line of each one instead: a line without `=`, an invalid key, an unclosed quote, and text after a closing quote.

:::warning
A `.env` file holds real credentials. Committing it publishes them to everyone with access to the repository.

Ignore every `.env` file and commit only `.env.example`:

```text title=".gitignore"
.env
.env.*
!.env.example
```
:::

### The APP_KEY

`APP_KEY` is your App's secret key. Tinka's encryption package, which comes next, will use it to encrypt data and to sign cookies and URLs. Generate it with `act generate:key`, which writes a random 256-bit key to `.env` and adds an empty `APP_KEY=` to `.env.example`, so your team knows to set one without seeing yours:

```sh
act generate:key
```

```text title="Output"
Wrote APP_KEY to .env and .env.example
```

Anyone who has the key can read what it encrypted and forge what it signs, so keep it out of the repository and use a different key in each environment. Production reads no `.env` file, so when `APP_ENV` is `production` the Command prints the key instead of writing it; set it in your platform's environment. `act generate:key --show` prints a key in any Mode. If a key leaks, generate a new one: data encrypted or signed with the old key can no longer be read.

### Using environment variables in your App

Read environment variables once, at boot, into a configuration struct, and pass that struct to the code that needs it. Code that calls `os.Getenv` deep inside a handler hides what the App depends on and fails long after startup. `env.Load` returns an Env, each getter reads one variable as a Go type, and `Err` reports every problem at once:

```go title="config.go"
package main

import (
	"net/url"
	"time"

	"github.com/mucolabs/tinka/env"
)

type Config struct {
	Port        int
	AppURL      *url.URL
	AppKey      env.Secret
	ReadTimeout time.Duration
}

func loadConfig() (Config, error) {
	e := env.Load()
	cfg := Config{
		Port:        e.Int("PORT", 3333),
		AppURL:      e.URL("APP_URL"),
		AppKey:      e.Secret("APP_KEY"),
		ReadTimeout: e.Duration("READ_TIMEOUT", 10*time.Second),
	}
	return cfg, e.Err()
}
```

`main` stops the App when the configuration is wrong:

```go title="main.go"
package main

import (
	"fmt"
	"os"
)

func main() {
	cfg, err := loadConfig()
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	fmt.Println("listening on port", cfg.Port)
}
```

Run it with an invalid port and no `APP_URL` or `APP_KEY`, and the error lists all three problems:

```sh
PORT=80a go run .
```

```text title="Output"
env: 3 problems:
	PORT must be a whole number, got "80a" (environment)
	APP_URL is missing
	APP_KEY is missing
```

`env.Load` never changes the process environment: the values it reads stay inside the Env.

### Variable interpolation

A value in a `.env` file can use another variable with `$NAME`, which saves repeating a host or a port:

```dotenv title=".env"
HOST=localhost
PORT=3333
APP_URL=http://$HOST:$PORT
```

`APP_URL` reads as `http://localhost:3333`. Wrap the name in braces when it contains a dash or touches other text:

```dotenv title=".env"
BUCKET=blog
REGION=eu-west-1
ASSETS_URL=https://${BUCKET}.s3.${REGION}.amazonaws.com
```

A reference can point to a variable in another `.env` file or in the real environment, which wins when both define it. A variable that isn't set anywhere expands to an empty string, and two variables that refer to each other are reported. Write `\$` for a literal dollar sign, or single-quote the value, which is never expanded, as in a shell:

```dotenv title=".env"
PRICE=\$99
PASSWORD='pa$$word'
```

### Reading values from files

Docker and Kubernetes mount secrets as files, so the secret never sits in the environment. When `KEY` is missing and `KEY_FILE` is set, the getter reads the value from the file `KEY_FILE` names:

```dotenv title=".env"
DB_PASSWORD_FILE=/run/secrets/db_password
GCS_KEY_FILE=gcs_key.json
```

`e.Secret("DB_PASSWORD")` returns the file's contents, with one trailing newline dropped. A relative path is resolved against the directory the `.env` files are loaded from. A file that can't be read is reported at boot. `KEY` itself always wins over `KEY_FILE`.

An identifier resolves values that start with a prefix you choose, such as `base64:`. Pass `env.Identifier` to `env.Load` with the function that turns the rest of the value into the real one:

```go
e := env.Load(env.Identifier("base64", func(v string) (string, error) {
	b, err := base64.StdEncoding.DecodeString(v)
	return string(b), err
}))
token := e.Secret("API_TOKEN") // API_TOKEN=base64:czNjcmV0 reads as s3cret
```

An identifier applies to values from the real environment and from the files alike. A value it can't resolve is reported at boot. Write `base64\:` to keep a value that starts with `base64:` as it is. Identifiers belong to the Env you pass them to, so two Apps in one test binary never share them.

### Environment-specific .env files

The **Mode** says how an App runs: `development`, `test` or `production`. It comes from `APP_ENV`, defaults to `development`, and decides which `.env` files `env.Load` reads:

| File | Purpose | Git status |
|---|---|---|
| `.env` | Base variables for every Mode | Ignored |
| `.env.local` | Local overrides, loaded in every Mode except `test` | Ignored |
| `.env.development` | Variables for development | Ignored |
| `.env.test` | Variables for tests, loaded when `APP_ENV=test` | Ignored |
| `.env.{mode}.local` | Local overrides for one Mode, such as `.env.test.local` | Ignored |
| `.env.example` | Every variable the App reads, with placeholder values | Committed |

When a variable is defined in several files, the file with the higher priority wins. For `APP_ENV=development`, from the highest priority:

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

For `APP_ENV=test`:

1. `.env.test.local`
2. `.env.test`
3. `.env`

`.env.local` isn't loaded in `test`, so your local overrides can't change what the test suite sees. A variable that's already in the real environment wins over every file, so your platform or CI can override a value without editing a file. A variable set to an empty value, such as `SENTRY_DSN=`, still replaces the same variable from a lower priority file, and then counts as missing.

:::warning
In production, `env.Load` reads no `.env` file at all, not even `.env.production`. A `.env` copied into a production image is ignored, and the App fails at boot with every variable it expected from that file listed as missing.

Set production configuration in the real environment instead:

```sh
APP_ENV=production APP_URL=https://blog.example.com go run .
```
:::

`APP_ENV` itself must come from the real environment, because it decides which files to read; setting it in a `.env` file is reported. Read the Mode with `e.Mode()`, which returns `env.Development`, `env.Test` or `env.Production`.

`env.Load` reads the files from the working directory. Set `ENV_PATH` in the real environment to read them from another directory, resolved against the working directory:

```sh
ENV_PATH=config/env go run .
```

In code, `env.Dir` sets the directory `ENV_PATH` is resolved against:

```go
e := env.Load(env.Dir("deploy/local"))
```

`.env.example` documents the App's variables for your team. Commit it with placeholder values, so a new developer copies it to `.env` and fills in their own:

```dotenv title=".env.example"
PORT=3333
APP_URL=http://localhost:3333
APP_KEY=your_app_key
MAIL_FROM=hello@example.com
STRIPE_KEY=your_stripe_test_key
```

### Validating environment variables

Every getter both reads and validates. A getter without a fallback reads a required variable, and `Err` reports it when it's missing. Pass a fallback to make the variable optional; the getter returns the fallback when the variable is missing. An empty value counts as missing. A value that's present but invalid is always reported, even with a fallback, because it's a mistake you want to see.

```go
e.Int("PORT")       // required
e.Int("PORT", 3333) // optional, 3333 when missing
```

Each getter converts to one type:

::::options

:::option{name="String" type="func(key string, fallback ...string) string"}
The value as it is.
:::

:::option{name="Int" type="func(key string, fallback ...int) int"}
A whole number in base 10, such as `3333`.
:::

:::option{name="Float" type="func(key string, fallback ...float64) float64"}
A finite number, such as `0.25`.
:::

:::option{name="Bool" type="func(key string, fallback ...bool) bool"}
`true` or `false`, also `1`, `0`, `t`, `f`, and their upper and title case forms.
:::

:::option{name="Duration" type="func(key string, fallback ...time.Duration) time.Duration"}
A `time.Duration` written as `30s`, `5m` or `1h30m`. A number without a unit is refused.
:::

:::option{name="URL" type="func(key string, fallback ...string) *url.URL"}
An absolute URL with a scheme and a host, such as `https://blog.example.com`.
:::

:::option{name="Host" type="func(key string, fallback ...string) string"}
A hostname or an IP address, such as `db.internal` or `10.0.0.5`.
:::

:::option{name="Email" type="func(key string, fallback ...string) string"}
A bare email address, such as `hello@example.com`.
:::

:::option{name="UUID" type="func(key string, fallback ...uuid.UUID) uuid.UUID"}
A UUID, returned as a `uuid.UUID`.
:::

:::option{name="OneOf" type="func[T ~string](key string, choices []T, fallback ...T) T"}
One of `choices`, returned as their own string type, so the result needs no conversion:

```go
type LogFormat string

format := e.OneOf("LOG_FORMAT", []LogFormat{"text", "json"}, "text")
```
:::

:::option{name="Secret" type="func(key string, fallback ...string) env.Secret"}
The value as an `env.Secret`, for API keys and passwords. Printing it with `fmt`, logging it with `log/slog` and encoding it as JSON all show `[redacted]`, so it can't leak through a log line or an error message. Call `Reveal` only where the value is used:

```go
key := e.Secret("STRIPE_KEY")
slog.Info("payments ready", "key", key) // key=[redacted]
client := stripe.New(key.Reveal())
```

Pass `""` as the fallback to make a secret optional. Once revealed, the plain string is no longer protected.
:::

::::

A problem quotes the rejected value so you can fix it, except from `String`, `Secret` and `URL`, which can hold credentials.

To require a variable in one Mode only, branch on the Mode:

```go
dsn := e.Secret("SENTRY_DSN", "")
if e.Mode() == env.Production {
	dsn = e.Secret("SENTRY_DSN")
}
```

`env.Map` builds an Env from a map, without reading the process environment or any file. Tests that use it can run in parallel, which `t.Setenv` doesn't allow:

```go title="config_test.go"
package main

import (
	"testing"

	"github.com/mucolabs/tinka/env"
)

func TestPortFallback(t *testing.T) {
	t.Parallel()
	e := env.Map(map[string]string{"APP_ENV": "test"})
	if got := e.Int("PORT", 3333); got != 3333 {
		t.Errorf("PORT = %d, want 3333", got)
	}
}
```

To test a function like `loadConfig` this way, have it take the Env as a parameter, and pass `env.Load()` from `main`.

### Writing .env files from a Command

A Command that generates a value, such as a new `APP_KEY`, records it with an `env.Editor`. `env.Edit` opens `.env` and `.env.example` in the directory `env.Load` reads from, and `Save` writes them back with every other line, comment and blank line unchanged:

```go title="commands/key.go"
package commands

import "github.com/mucolabs/tinka/env"

// SaveKey records key as the App's APP_KEY.
func SaveKey(key string) error {
	ed, err := env.Edit()
	if err != nil {
		return err
	}
	if err := ed.SetSecret("APP_KEY", key); err != nil {
		return err
	}
	return ed.Save()
}
```

`Set` writes the value to both files. `SetSecret` writes it to `.env` and leaves it empty in `.env.example`, so the example names the variable without revealing it. An existing key is updated in place and a new one is added at the end. The value is quoted when it needs to be, so `env.Load` reads it back exactly. A file that doesn't exist is never created; `ed.Files()` lists the ones found.

To read a `.env` file without loading it, `env.Parse` returns its variables with references between them expanded, and `env.Paths` lists the files `env.Load` reads for a Mode, from the highest priority.

See also: [Commands](reference/commands.md#act-generatekey), [Installation](installation.md)
