# Settings: `config.jadpo`

A setting is a value that can differ between deployments, such as the
database's address or the address emails are sent from. Every setting your
app needs is a field of one `config { }` declaration, in the file
`config.jadpo` at the project's root. A field has a type and, optionally,
`secret: true`, a `default:`, a `binding:` and a `test:` value, each
explained below. Code reads a setting as `config.<field>` anywhere,
functions included, because a setting can't change while the app runs.

```jadpo
config {
    browser_origin: Url
    database_url: PostgresUrl
    mail_sender: Email
    session_signing_key: SigningKey
    soft_delete_retention: Duration {
        default: 30d
    }
}
```

Each value comes from an environment variable named after the field in
capitals: `mail_sender` reads `MAIL_SENDER`. The declaration has no name,
since a project has only one. There is no `env.jadpo` and no second file
that maps settings to variables.

A field is required unless it has a `default:`. There are no optional
settings: give the field a default, or leave the feature's setting
required.

What code reads has the field's type. `config.page_size` below is the
`ListLimit` the field declares, checked when the app starts, so code never
builds it again. Declare the field with the named type your code uses
(`page_size: ListLimit { default: 25 }`) and read `config.page_size`
wherever a `ListLimit` goes. You can declare a setting with a built-in type
such as `Duration` or `Int`, but a function's parameters and results take
named types ([Signatures use named types](signatures-use-named-types.md)).
Building a named type from a plainly typed setting is an ordinary
construction, which can fail, so it needs `attempt`.

```jadpo
type ListLimit = Int {
    min: 1
    max: 100
}

type FromAddress = Email {}

config {
    mail_sender: FromAddress {
        test: "reminders@example.test"
    }
    page_size: ListLimit {
        default: 25
    }
}

function first_page() -> ListLimit {
    return config.page_size
}

function from_address() -> FromAddress {
    return config.mail_sender
}
```

## Environment variable names

By default the variable's name is the field's name in capitals. Write
`binding:` only when it must be something else, for example to keep a name
a hosting platform already sets:

```jadpo
config {
    database_url: PostgresUrl {
        binding: "HEROKU_POSTGRESQL_URL"
    }
}
```

A binding is capital letters, digits and `_`, starting with a capital
letter, and no two settings read the same variable. A `SigningKey` setting
also reads its variable with `_PREVIOUS` after it
([Signing keys](signing-keys.md)), so no other setting may bind that name.

Because the name comes from the field, renaming a field renames its
variable, and a deployment that still sets the old name breaks. So a
changed variable name is shown for review like any other change to the
app's contract ([Records and manifest](records-and-manifest.md)).

## What a setting can hold

A setting is read from one environment variable, so its type must be one
that can be read from a single piece of text:

- text-based types, `Int`, `Decimal`, `Bool`, `Uuid`, `Email`, `Url`,
  `IpAddress`, `Duration`, `Instant` and `CalendarDate`, and any named type
  built on one of them;
- an enum whose variants carry no data, and the built-in enums `Zone` and
  `Locale`;
- a `ModelId`, read from its text such as `openai/gpt-4.1-mini`, since every
  model an app's `ai:` names is one ([AI](ai.md));
- a `Money`, read from its canonical text such as `"GBP 20.00"`, since an AI
  budget, `ai: { limits: { per_day: { cost: } } }`, names one;
- a `Certificate`, read from its PEM text, since a database connection's
  `authority:` names one ([PostgreSQL over TLS](stores.md#postgresql-over-tls)).

Lists, objects, maps, `T?` and enums whose variants carry data are refused
(`configuration.type_unsupported`). Split such a value into several fields.

```jadpo
type Verbosity = Enum {
    verbose
    normal
    terse
}

config {
    business_zone: Zone {
        default: Zone.europe_london
    }
    log_level: Verbosity {
        default: Verbosity.normal
    }
}
```

Here `LOG_LEVEL=verbose` and `BUSINESS_ZONE=Europe/London` are valid values.
Each type is written this way in a variable:

- an enum as the variant's name;
- a `Zone` as its IANA name, such as `Europe/London`;
- a `Locale` as its BCP 47 tag, such as `en-GB`;
- a `Duration` as it is in source (`30s`, `5m`, `2h`, `30d`) or as its
  ISO 8601 text (`PT5M`);
- a `Bool` as `true` or `false`;
- an `Instant` in ISO 8601 ending in `Z`, never with an offset, and a
  `CalendarDate` in ISO 8601.

A variable set to nothing counts as not set.

## Defaults and test values

A default is a literal of the field's type, such as `default: 25`,
`default: 5s` or `default: Verbosity.normal`. The compiler checks it against
every rule of the type and of the types it is built on. `test:` is the value
every test reads, and is checked the same way. A field with no default that
a test reads needs a `test:` value
([Configuration in tests](configuration-in-tests.md)).

Neither can name a [constant](constants.md), even though a
constant is also fixed when the code compiles: a value is a constant or a
setting, never both ([A constant or a setting](settings-config-jadpo.md#a-constant-or-a-setting)).

A secret can have neither. So a production secret someone forgot to set
stops the app, instead of falling back to a value written in source, and
secrets never appear in test code.

## A constant or a setting

A value that could differ between deployments is a setting. One that never
does is a [constant](constants.md). A value is one or the other,
never both, so it has one source and one way to read it: a constant is read
as `security.max_login_attempts`, a setting as `config.max_login_attempts`.

That is why a `default:` never names a constant. If it could, code reading
the constant directly would quietly ignore the environment. Say
`security.standard_attempts` (5) were `max_login_attempts`'s default, and
someone set `MAX_LOGIN_ATTEMPTS=10`: code reading
`security.standard_attempts` would still allow 5. `default:` on the field
already names the default, in the one place a reader looks.

There are three kinds of value, from fixed to changing while the app runs:

| Kind | Where | Changes | Read as |
|---|---|---|---|
| `constant` | Beside its use, or in a library ([Constants](constants.md)) | With a code change | `security.max_login_attempts` |
| Setting | `config { }` in `config.jadpo` | Per deployment, when the app starts | `config.max_login_attempts` |
| Flag | `flags { }` in `flags.jadpo` ([Feature flags](feature-flags.md)) | While the app runs | `flags.new_checkout` |

### Making a constant a setting

When a fixed value needs to differ per deployment, a tool makes the change
for you: `jadpo config move <constant>` on the command line, or "Make this
a setting" in the editor on the constant's declaration or any read of it.
Both run the same code, as the editor's rename runs `jadpo rename`
([Editor support](editor-support.md)). The move is offered,
never required, so `jadpo fmt --layout`, which makes only the moves an
error asks for, doesn't make it. In one step it:

- adds a field to the one `config { }`, before the first field whose name
  comes after it (in a new `config.jadpo` when there is none), with the
  constant's name and type, whose `default:` is the constant's value as
  written when that is a literal (`5`, `30d`, `0.2000`,
  `Verbosity.normal`) and otherwise the value the compiler worked out, and
  whose `///` lines are the constant's;
- deletes the constant, and a library whose only member it was, and a file
  left holding nothing (with its folder, if that is left empty);
- rewrites every read, `security.max_login_attempts` (or
  `max_login_attempts` beside its declaration) to
  `config.max_login_attempts`, and fixes the imports of every file it
  touches, removing a `use` line nothing needs any more.

`<constant>` names the constant as code reads it, `security.max_login_attempts` or
`free_delivery_from`, or after its domain
(`accounts.security.max_login_attempts`) when private constants of two
domains share the name; a name that fits two is refused, listing each.

Before, a library in the `accounts` domain holds the constant and an
action reads it:

```jadpo
use accounts/types { LoginAttempts }

library security {
    /// Failed sign-ins before an account is locked.
    constant max_login_attempts: LoginAttempts = 5
}
```

```jadpo
use accounts/types { AccountLocked, LoginAttempts }
use accounts/libraries { security }

action sign_in(failed_attempts: LoginAttempts) -> AccountLocked {
    return AccountLocked(failed_attempts >= security.max_login_attempts)
}
```

The move lists what it changed, file by file (the setting added or filled,
the constant and anything removed with it, and each read by line; imports
are left to the diff), then the setting and the variable a deployment can
now set:

```text
$ jadpo config move security.max_login_attempts
  config.jadpo
    + max_login_attempts  setting, default 5

  domains/accounts/libraries/security.jadpo
    - max_login_attempts  constant
    - security            library, nothing left in it
    - security.jadpo      file, nothing left in it

  domains/accounts/actions/sign_in.jadpo
    ~ line 5              reads config.max_login_attempts

Made security.max_login_attempts a setting
    setting   config.max_login_attempts
    variable  MAX_LOGIN_ATTEMPTS
```

After, the library held nothing else, so it and its file are gone, and
the imports follow:

```jadpo
use accounts/types { LoginAttempts }

config {
    /// Failed sign-ins before an account is locked.
    max_login_attempts: LoginAttempts {
        default: 5
    }
}
```

```jadpo
use accounts/types { AccountLocked, LoginAttempts }

action sign_in(failed_attempts: LoginAttempts) -> AccountLocked {
    return AccountLocked(failed_attempts >= config.max_login_attempts)
}
```

Every read changes, so a reviewer sees each place whose behaviour can now
differ per deployment, and the tool does the rewriting. The new variable,
`MAX_LOGIN_ATTEMPTS`, comes from the field's name as any setting's does.

The move refuses, editing nothing and saying why, for:

- a constant whose type a setting can't hold ([What a setting can
  hold](#what-a-setting-can-hold): a list, an object, `T?`, an enum with
  data);
- a constant whose name a setting already has, naming that setting;
- a constant read where a setting can't be: in a query, an update by `id`,
  a backfill or another constant's value, listing each such read.

Each reason is an error of its own: `configuration.move_type_unsupported`,
`configuration.move_name_taken` and `configuration.move_read_unsupported`,
which are [command errors](command-errors.md). They are all
given at once, ending `config move refused: nothing changed`.

A project with other errors isn't moved: its errors are shown as `jadpo
check` shows them, and the editor shows the action as unavailable. A result
that wouldn't check (another constant left unused, say) is undone and
refused with the errors it would have, and nothing is written.

When exactly one setting's `default:` names the constant
(`configuration.default_literal_required`, below), that error alone
doesn't stop the move: that setting is the one the constant becomes, its
`default:` becomes the constant's value, and every read becomes
`config.<that setting>`.

Moving a setting back to a constant has no command. It is rare, and
removing a setting changes every deployment's environment, so you make it
by hand.

Jadpo has no constant that an environment variable can override, no
`config { }` block per domain, no settings gathered from beside their use,
and no values per environment in source (`production:`, `staging:`). Each
would make a value look fixed where it differs per deployment, or lose the
one list of variables an operator sets ([Values on your machine and in
production](#values-on-your-machine-and-in-production)).

## Fields in name order

`jadpo fmt` sorts the fields of `config { }` by name, each with the comments
written above it and after it on its line. So two changes that each add a
setting rarely touch the same lines, and they merge cleanly.

A setting's position means nothing: it is read, bound and reported by name.
`jadpo config check` and the check before the app starts list settings in
the order they are written, which is name order once formatted. `jadpo fmt
--check` fails on a `config { }` out of order. The examples on these pages
are in name order.
