# 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.

> **Important**
>
> Don't put credentials in config files. Store them as [secrets](/docs/rs/primitives/secrets), and reference them from a config [secret field](#secrets) 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`:

```rust
-- 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`:

```toml
-- 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:

| Option | Applies to | Meaning |
|---|---|---|
| `min = N`, `max = N` | numbers | The least and greatest value allowed |
| `min_len = N`, `max_len = N` | strings | The fewest and most characters |
| `pattern = "regex"` | strings | A regular expression the value must match |
| `min_items = N`, `max_items = N` | lists | The fewest and most items |
| `secret = "Name"` | `Secret` fields | The field reads this secret; see [Secrets](#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()`:

```rust
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.

```rust
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`:

```rust
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](/docs/rs/primitives/secrets#scopes), the type you declare a config with decides who it belongs to:

| Type | Scope | Value files live |
|---|---|---|
| `Config` | Belongs 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` |
| `SharedConfig` | One value for the whole app. Any service can read it, and each one that does is recorded as using it. | Next to `encore.app` |
| `GlobalConfig` | One value for the whole app, available to every service without being recorded. | Next to `encore.app` |

```rust
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`:

| File | Applies to |
|---|---|
| `checkout.config.toml` | Every environment |
| `checkout.production.config.toml` | Environments of a type: `local`, `test`, `development`, `preview`, or `production` |
| `checkout.@prod-eu.config.toml` | The environment named `prod-eu` |
| `checkout.user.config.toml` | Only 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](https://www.rfc-editor.org/rfc/rfc7396): tables merge, any other value replaces the one below it, and `null` removes it. An overlay only needs the fields it changes:

```toml
-- 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](/docs/rs/primitives/secrets) instead of holding a value. Declare the field as `encore::secrets::Secret` and name the secret with `#[config(secret = "...")]`:

```rust
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:

| Command | What it does |
|---|---|
| `encore config list` | Lists 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 validate` | Checks 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 link` | Adds 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:

```shell
$ 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:

```rust
#[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](/docs/rs/develop/testing) for more.
