Configuration

Three separate things are called "configuration", and they answer different questions:

What it is Where it lives Selected by
Environments The typed matrix of environments the system has, and what differs between them Config(...) in the config file --env
Inputs A per-run value from outside the repository -var, --var-file, [inputs] supplied per run
Profiles Where a run points: state backend, AWS profile, parallelism [profile.<name>] in atlantide.toml --profile/-P

--profile and --env are orthogonal; neither implies the other.

The project file

Set the config path, the state database and any defaults once in atlantide.toml, and commands inside the project need no flags.

The file is looked up in the working directory and then in each parent, the way git locates a repository root. Relative paths inside it resolve against the directory holding the file, not the directory you ran the command from.

config       = "infra.py"
state        = "atlantide.db"
parallelism  = 8
aws_region   = "eu-north-1"
aws_profile  = "default"
aws_endpoint = "http://localhost:4566"   # send every AWS call here instead
secrets_key   = ".atlantide.key"
secrets_store = ".atlantide.secrets"

[aws.aliases.prod]                       # alternate account (multi-account)
profile  = "prod-account"                # a resource selects it via provider_alias="prod"

Environments

A Config declares every environment the system has, and what differs between them, once and in one place:

from atlantide.core import Config, EnvSchema, Stack

class AppEnv(EnvSchema):
    domain: str
    size: int = 1

config = Config(
    AppEnv,
    envs={
        "dev":  {"region": "eu-north-1", "domain": "dev.example.io"},
        "prod": {"region": "us-east-1",  "domain": "example.io", "size": 5},
    },
)

for env in config.envs():                     # env: AppEnv
    with Stack(env.name, config=env):
        ...

Each variable declares its type and, optionally, a default. Everything is checked when the Config is constructed, so a missing or mistyped prod value fails atlantide validate in CI rather than at the moment prod is applied.

An environment name becomes the stack name, so it must be an identifier.

The schema

EnvSchema is the one class Atlas-lang admits — a module-level class whose body is annotated fields only, no methods, no decorators and no metaclass, so it is still data. Declaring it is what makes the variables ordinary attributes: an editor completes env.domain, and env.domian is a type error before anything runs.

A field may be annotated str, int, float, bool, list or dict, or X | None to make it optional and nullable. Parameterized generics such as list[str] are not supported.

The schema can instead be a mapping of var() declarations, when the static side does not matter:

from atlantide.core import Config, var

config = Config(schema={"size": var(int, default=1)}, envs={...})

Both forms run the same validation; the class form adds the static knowledge. A var() without a default is required — every environment must supply it, and a missing one is reported when the Config is built rather than wherever the value is eventually read. default=None instead makes it optional.

Well-known keys

region, tags and name_prefix are implicit in every schema, so Stack(env.name, config=env) needs no separate region=:

with Stack(env.name, config=env):          # region/tags/name_prefix from the env
    ...

An explicit Stack argument wins over the environment, except tags, which merge (the stack's own winning). A schema may re-declare a well-known key to make it required or to narrow it.

Inside the body the environment is ambient — a component reads it with current_config() rather than having it threaded through a constructor.

Selecting one

atlantide plan  infra.py --env prod            # prod only
atlantide apply infra.py --env dev --env prod  # repeatable

An unselected environment is out of scope, not undeclared: its state is never diffed and never planned for deletion, and the plan says what it left out.

envs: prod (of dev, prod) — dev is not planned and will not change

Naming an environment the config does not declare is an error, rather than a run that silently does nothing. Stacks declared outside the config.envs() loop — a shared common — are not part of the matrix and are unaffected by --env, which is what keeps a shared VPC in the graph when a run is narrowed.

--env is accepted by plan, apply, validate, build and destroy. On destroy it selects by stack rather than through the config: destroy reads no config, only state, and an environment's stack is its name.

build records the selection in the .atlas artifact, so a deploy covers exactly the environments the build did. Determinism is over (config, inputs, selected environments).

Inputs

An input is a per-run value supplied from outside the repository — a CI build number, a fork's name prefix — and arrives as text:

env = atlantide.input("env", "dev")      # with a default
count = int(atlantide.input("count"))    # values arrive as strings from -var
atlantide plan -var env=prod
atlantide plan --var-file prod.toml

Inputs can be set in three places, in increasing order of precedence: [inputs] in atlantide.toml (or [profile.<name>.inputs] for one profile), then --var-file, then -var on the command line.

Every plan prints the inputs it actually read. Only inputs the config reads affect the result, so an unused value cannot change what a run appears to be.

An input is not an environment

What differs between environments belongs in a Config: it is checked in, typed and validated eagerly. What differs between runs of one environment is an input. The two compose — an environment's value may be built from an input.

Inputs are not for secrets

atlantide.secret("name") returns a handle, not a value. Passing a secret as an input would write the plaintext into the IR, into build artifacts and into state.

Profiles

A [profile.<name>] table overlays the top level, and --profile/-P — or the ATLANTIDE_PROFILE environment variable — selects it. A profile decides where a run points: which state backend, which AWS account, how much parallelism.

The overlay is applied table by table, so a profile restates only the keys it changes and inherits the rest:

parallelism = 4

[profile.prod]
parallelism = 16

[profile.prod.state]                     # inherits the base [state] keys it omits
backend    = "s3"
bucket     = "acme-atlantide-state"
key        = "prod/atlantide.json"
lock_table = "atlantide-locks"

Naming a profile the file does not define is an error rather than a silent fall-through to the defaults.