Configuration

Define behavior in specific environments

Configuration lets you set values your code reads at runtime, such as limits, feature flags, and endpoints, and override them for specific environments without changing the code.

In Encore.rs a config is a Rust type. Encore derives a schema from the type, you write the values in TOML or JSON files next to your code, and Encore checks every value against the schema before your service sees it.

Take care

Don't put credentials in config files. Store them as secrets, and reference them from a config secret field if needed.

Declaring a config

Define a type that derives serde's Deserialize and Encore's ConfigType, and declare the config as a module-level static:

orders/src/checkout.rs
use encore::config::{Config, ConfigType, Duration}; use serde::Deserialize; #[derive(Deserialize, ConfigType)] struct Checkout { /// Most items one order may hold. #[config(min = 1, max = 500)] max_items: u32, #[serde(default)] promo: Option<Promo>, payment_timeout: Duration, } #[derive(Deserialize, ConfigType)] struct Promo { code: String, discount: u8, } static CHECKOUT: Config<Checkout> = Config::new("checkout"); fn too_many(items: usize) -> bool { items > CHECKOUT.get().max_items as usize }

Then write its value in a file named after the config, next to the service's encore.service.json:

orders/checkout.config.toml
max_items = 50 payment_timeout = "1m30s" [promo] code = "FALL26" discount = 10

Encore reads config declarations and their types from your source code, so a few rules apply:

  • A config must be declared as a module-level static, with its value type named in the static's type: static CHECKOUT: Config<Checkout>.
  • The name must be a string literal. It's an identifier: a letter or underscore, followed by letters, digits, and underscores.
  • The type, and every type it contains, must derive Deserialize. Derive ConfigType on any struct whose fields use #[config(...)].

The type is the schema

Encore builds a JSON Schema from your type: its fields and their types, serde attributes such as rename, rename_all, default, and flatten, enum tagging, the #[config(...)] constraints, and doc comments, which become field descriptions.

Every value is checked against that schema: while you edit a file under encore run, on deploy, and when the service starts. A service won't start with a value it can't read, which is why get() can't fail.

Supported types

  • bool, the integer types up to 64 bits, f32, f64, char, and String
  • Option<T>, Vec<T>, VecDeque<T>, HashSet<T>, BTreeSet<T>
  • HashMap<K, V> and BTreeMap<K, V> with string keys
  • Box<T>, Arc<T>, Rc<T> (including Box<str> and Arc<str>), and PathBuf
  • serde_json::Value and serde_json::Map for free-form data
  • encore::config::Duration for lengths of time
  • Your own structs (with named fields, or newtypes) and enums, except #[serde(untagged)] enums

Encore reports an error at the part of the type it can't describe, for example a tuple, a borrowed &str, or std::time::Duration.

Constraints

#[config(...)] on a field adds a constraint to the schema:

OptionApplies toMeaning
min = N, max = NnumbersThe least and greatest value allowed
min_len = N, max_len = NstringsThe fewest and most characters
pattern = "regex"stringsA regular expression the value must match
min_items = N, max_items = NlistsThe fewest and most items
secret = "Name"Secret fieldsThe field reads this secret; see Secrets

Durations

encore::config::Duration is written as a number and a unit, or several: "30s", "250ms", "1h30m", "1.5h". The units are ns, us (or µs), ms, s, m, and h. It dereferences to a std::time::Duration, and converts into one with .into():

let timeout: std::time::Duration = CHECKOUT.get().payment_timeout.into();

Defaults

A field that no file sets takes its default from your code: #[serde(default)], #[serde(default = "path")], or None for an Option. If no file gives a config a value at all, get() returns the defaults.

Reading a config

get() returns a Value<T>: a snapshot of the config's current value that dereferences to T. It's cheap to make and to clone.

let checkout = CHECKOUT.get(); if let Some(promo) = &checkout.promo { apply_discount(&promo.code, promo.discount); }

A config's value can change while the app runs. Edit its file under encore run and the running service has the new value within a second, without a restart. A snapshot keeps the value it was made with, so hold one for as long as you need a consistent view (for example, one request), and call get() again for the next.

Reacting to changes

If you build something from a config, such as an HTTP client or a connection pool, rebuild it when the value changes with on_change:

CHECKOUT.on_change(|checkout| { println!("max items is now {}", checkout.max_items); });

The callback is called with each new value from then on. It runs on the runtime's threads, so keep it quick and spawn anything that waits.

Scopes

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

TypeScopeValue files live
ConfigBelongs to the service that declares it. Two services can each declare a config with the same name. It must be declared inside a service.Next to the service's encore.service.json
SharedConfigOne value for the whole app. Any service can read it, and each one that does is recorded as using it.Next to encore.app
GlobalConfigOne value for the whole app, available to every service without being recorded.Next to encore.app
use encore::config::{ConfigType, SharedConfig}; use serde::Deserialize; #[derive(Deserialize, ConfigType)] struct Flags { #[serde(default)] new_checkout: bool, } static FLAGS: SharedConfig<Flags> = SharedConfig::new("flags");

Environment-specific values

A config's value is its base file plus any overlay files that apply to the current environment. Each file can be .toml or .json:

FileApplies to
checkout.config.tomlEvery environment
checkout.production.config.tomlEnvironments of a type: local, test, development, preview, or production
checkout.@prod-eu.config.tomlThe environment named prod-eu
checkout.user.config.tomlOnly your own encore run

Overlays are applied in that order, lowest first, on top of the defaults in code. They're merged field by field using JSON Merge Patch: tables merge, any other value replaces the one below it, and null removes it. An overlay only needs the fields it changes:

orders/checkout.production.config.toml
max_items = 200

Each layer is checked against the schema on its own, so a mistake is reported in the file that makes it, and then the merged value is checked in full.

The user override (<name>.user.config.toml) is for your own values on your own machine. It applies to encore run only, never to tests or deployed environments. Keep it out of version control: encore app new and encore app init add *.user.config.* to the app's .gitignore.

Secrets

A config field can read a secret instead of holding a value. Declare the field as encore::secrets::Secret and name the secret with #[config(secret = "...")]:

use encore::config::{ConfigType, GlobalConfig}; use encore::secrets::{GlobalSecret, Secret}; use serde::Deserialize; #[derive(Deserialize, ConfigType)] struct Payments { stripe: Stripe, } #[derive(Deserialize, ConfigType)] struct Stripe { endpoint: String, #[config(secret = "StripeKey")] api_key: Secret, } static PAYMENTS: GlobalConfig<Payments> = GlobalConfig::new("payments"); // The secret is declared in code as any secret is, with the config's scope. static STRIPE_KEY: GlobalSecret = GlobalSecret::new("StripeKey"); fn stripe_key() -> Result<String, encore::Error> { Ok(PAYMENTS.get().stripe.api_key.value()?.to_string()) }
  • The field's type is always Secret; the secret has the config's own scope. A Config reads its service's own secret, a SharedConfig a shared one, and a GlobalConfig a global one.
  • The secret must also be declared in code (Secret::new, SharedSecret::new, or GlobalSecret::new). A secret field that names an undeclared secret is an error.
  • No config file sets the field. The secret holds the value, per environment, as secrets do.
  • A secret field can't be inside a list, a map, or an enum.

Inspecting and editing config

The encore config command works with your app's configs:

CommandWhat it does
encore config listLists the configs the app declares, and whether each value for encore run is valid
encore config show <name>Shows a config's effective value and which file and line each value comes from. --env-type and --env pick the environment.
encore config schema <name>Prints the JSON Schema the config's values are checked against
encore config validateChecks every config file for every environment type, and exits non-zero if any value is invalid
encore config set <name> <path> <value>Sets one value, such as max_items or promo.code, keeping the rest of the file as it is
encore config unset <name> <path>Removes one value, so a lower layer or the default applies
encore config linkAdds a schema reference to each config file, so editors check and complete what you write

A service's own config is named <service>.<name>, for example encore config show orders.checkout.

set and unset write to the user override by default. Pass --base, --env-type <type>, or --env <name> to change another layer:

$ encore config set orders.checkout max_items 100
$ encore config set orders.checkout max_items 200 --env-type production

Testing

Under encore test, a config's value is what the files say for the test environment type. Use a checkout.test.config.toml overlay to change values for all tests.

To give one test a value of its own, use encore::testing::set_config. No other test sees it, and it's gone when the test ends. The value is checked against the config's schema:

#[encore::test] async fn rejects_large_orders() -> Result<(), encore::Error> { encore::testing::set_config( &CHECKOUT, serde_json::json!({"max_items": 2, "payment_timeout": "30s"}), )?; assert!(too_many(3)); Ok(()) }

See Testing for more.