Configuration

Typed settings that vary by environment

A config is a typed value your code reads, such as limits, timeouts and feature flags. You declare its type in Python and write its value in a TOML or JSON file beside the code, with overlays that change it for some environments only. Encore checks every value against the type before your service starts, and under encore run you can change a value without restarting anything.

Take care

For sensitive data, use secrets instead. A config can read secrets but never holds them in its files.

Declaring a config

Describe the config's value with a dataclass, and declare the config as a module-level variable:

orders/settings.py
from dataclasses import dataclass, field from datetime import timedelta from typing import Annotated from encore.config import Config, Max, Min @dataclass class Checkout: max_items: Annotated[int, Min(1), Max(500)] currencies: list[str] = field(default_factory=lambda: ["USD"]) payment_timeout: timedelta = timedelta(seconds=90) checkout = Config[Checkout]("checkout")

Then write its value in a file named after it, beside the service's encore.service.json:

orders/checkout.config.toml
max_items = 100 payment_timeout = "2m"

Read the value with .value. It's an instance of your type:

from orders.settings import checkout def validate_cart(items: list[str]) -> None: limit = checkout.value.max_items if len(items) > limit: raise ValueError(f"at most {limit} items per order")

Encore reads config declarations from the source, so:

  • Declare configs at module level.
  • The name must be a string literal and an identifier: a letter or underscore, then letters, digits and underscores.
  • The type argument is required: Config[Checkout]("checkout"). Without it, reading .value raises TypeError.
  • Use the config by reading .value or calling .on_change(). Encore records which services read which configs from these calls, so it rejects passing the config object itself around.

Supported types

The config's type is a dataclass, a TypedDict or an Enum, built from:

  • str, int, float, bool
  • Literal[...], Optional[...], Enum subclasses
  • list[...] and dict[str, ...]
  • nested dataclasses and TypedDicts, including generic ones
  • typing.Any
  • datetime.timedelta, written as a duration string such as "30s", "250ms" or "1h30m"
  • Secret fields

A dataclass field with a default, or an Optional field, may be left out of the files. A union of dataclasses is supported when each has a Literal tag field that tells them apart. Encore reports a type it can't describe as an error at the part of the type that's the problem.

Constraints

Narrow what a field accepts with Annotated markers from encore.config:

MarkerApplies toExample
Min(n), Max(n)int, floatAnnotated[int, Min(1), Max(500)]
MinLen(n), MaxLen(n)strAnnotated[str, MaxLen(64)]
Pattern(regex)strAnnotated[str, Pattern("^[A-Z]+$")]
MinItems(n), MaxItems(n)listAnnotated[list[str], MinItems(1)]

Encore checks values against these before the service sees them. At run time the markers do nothing.

Config scopes

As with secrets, the class you declare a config with decides who it belongs to:

ClassBelongs toIts files live
Config[T]The service that declares it, which has its own value. Must be declared inside a service.Beside the service's encore.service.json
SharedConfig[T]The whole app, with one value. Encore records each service that reads it.Beside encore.app
GlobalConfig[T]The whole app, with one value, available to every service. Reads are not recorded.Beside encore.app
from dataclasses import dataclass from encore.config import SharedConfig @dataclass class Flags: new_checkout: bool = False flags = SharedConfig[Flags]("flags")

Environment-specific values

A config's value is merged from several files. Each layer overrides the ones before it, using JSON Merge Patch semantics:

  1. The defaults in your type.
  2. <name>.config.toml: the value in every environment.
  3. <name>.<type>.config.toml: environments of one type, where the type is local (encore run), test (encore test), development, preview or production.
  4. <name>.@<env>.config.toml: the environment named <env>.
  5. <name>.user.config.toml: your own overrides, applied to your encore run only. These files are gitignored (*.user.config.*) and never read in tests.

Each file can be .toml or .json. For example:

orders/checkout.config.toml
max_items = 100
orders/checkout.production.config.toml
max_items = 500 payment_timeout = "30s"
orders/checkout.@prod-eu.config.toml
currencies = ["EUR", "SEK"]

Each file is checked as a partial value, so a mistake is reported in the file that makes it, and the merged value is then checked in full. A file whose name doesn't match a declared config is ignored, unless its name is close to one, which is reported as a likely typo.

Reacting to changes

Under encore run, editing a config file updates the value in running services without a rebuild or restart. An invalid edit is reported, and services keep the last valid value.

Each read of .value returns the newest value. Keep the result in a local variable when you want one consistent value for the duration of an operation:

settings = checkout.value # one snapshot for this whole request

To act when the value changes, register a callback with on_change. It runs on a background thread, so keep it quick. It returns a function that stops the calls:

import logging log = logging.getLogger(__name__) def log_change(value: Checkout) -> None: log.info("checkout config changed", extra={"max_items": value.max_items}) stop = checkout.on_change(log_change)

An exception raised by the callback is logged, and later changes still reach it.

Secrets in config

A field of type Secret[str] or Secret[bytes] reads a secret instead of holding a value. Its default names the secret. The secret must also be declared in code, with the config's scope: a service's own Secret for a Config, a SharedSecret for a SharedConfig, and a GlobalSecret for a GlobalConfig.

orders/settings.py
from dataclasses import dataclass from encore.config import Config from encore.secrets import Secret stripe_key = Secret[str]("StripeKey") @dataclass class Payments: provider_url: str api_key: Secret[str] = Secret[str]("StripeKey") payments = Config[Payments]("payments") def auth_header() -> str: return f"Bearer {payments.value.api_key.value}"

The field's type is always Secret, whatever the config's scope. Secret fields can't be written in config files, and can't be Optional or be inside lists, dicts or unions.

Inspecting values with the CLI

The encore config commands show and edit config values:

$ encore config list # the app's configs, and whether each value is valid
$ encore config show orders.checkout # the merged value, and which file each part comes from
$ encore config show orders.checkout --env-type production
$ encore config validate # check every file, for every environment type
$ encore config schema orders.checkout # the JSON Schema Encore derived from the type

Name a service's config as <service>.<name>, and a shared or global one by its name alone.

encore config set and encore config unset change one value in one file, keeping the rest of the file as it was. They write your user override by default; pass --base, --env-type <type> or --env <name> to change another layer:

$ encore config set orders.checkout max_items 20
$ encore config set orders.checkout max_items 500 --env-type production
$ encore config unset orders.checkout max_items

Editor support

Run encore config link to name each config file's schema in the file: a #:schema comment in TOML, or a "$schema" key in JSON. Editors with JSON Schema support then check and complete what you write. The schemas are served by the local Encore daemon. Run it again after adding a file.

Testing

Under encore test, a config has the value its test overlay gives it: <name>.test.config.toml over the base file. User overrides are never applied in tests.