Env Profiles
A working dev env is rarely a single section. Secrets live in credentials.app,
datastore URLs in accessories.postgres / accessories.redis, and a few values
are plain literals. An env profile composes all of those into one named,
exportable env — declared once in config instead of re-derived in each bin/*.sh.
declaring a profile
A profile is an [env.<name>] table. Each key is an environment variable name;
each value is a string that goes through the full resolution pipeline —
interpolation, shell eval,
providers, and sources — so it can draw
from any section, provider, source bag, or literal.
[credentials.app]
service_secret = "secret://op://Private/app/service_secret"
google_o_auth_client_id = "op://Private/app/google_id"
google_o_auth_client_secret = "secret://op://Private/app/google_secret"
anthropic_api_key = "secret://op://Private/app/anthropic"
[accessories.postgres]
url = "secret://op://Private/postgres/url"
[accessories.redis]
url = "redis://localhost:6379"
# A named, exportable env profile
[env.dev]
SERVICE_SECRET = "{credentials.app.service_secret}"
POSTGRES_URL = "{accessories.postgres.url}"
REDIS_URL = "{accessories.redis.url}"
GOOGLE_O_AUTH_CLIENT_ID = "{credentials.app.google_o_auth_client_id}"
GOOGLE_O_AUTH_CLIENT_SECRET = "{credentials.app.google_o_auth_client_secret}"
ANTHROPIC_API_KEY = "{credentials.app.anthropic_api_key}"
HOST_NAME = "http://localhost:8000"
A {ref} to a secret:// value carries the secret flag through, so
SERVICE_SECRET above is still treated as a secret (masked by show, and
requiring --reveal from export).
Profile keys can reference a [sources] bag directly, which is
the efficient way to pull many values from one bulk export — the source’s load
command runs once for the whole profile, not once per key:
[sources]
infisical = "infisical export --env={vars.stage} --format=dotenv"
[env.dev]
DATABASE_URL = "infisical://DATABASE_URL"
API_KEY = "secret://infisical://API_KEY"
pinning vars
A profile can pin its own variables under a vars key. This
is handy when the profile’s values depend on a stage (or any var) that you’d
otherwise have to pass with --set every time. Use TOML dotted keys so the pins
sit right at the top of the profile:
[providers.op]
cmd = "op read {uri}"
[credentials.app]
service_secret = "secret://op://{stage}/app/service_secret"
[env.dev]
vars.stage = "development"
SERVICE_SECRET = "{credentials.app.service_secret}"
# no --set needed — the profile pins stage=development
confit export --profile dev --reveal --out .env.dev
vars is a sub-table, so it’s never emitted as an env var. Pinned vars slot into
the normal precedence as a default —
overriding the global [vars] section, but still overridden by CONFIT_VAR_*
and --set:
[vars] < profile vars.* < CONFIT_VAR_* < --set
# the profile pins development, but --set still wins
confit export --profile dev --set stage=staging --reveal
using a profile
Export the whole profile with confit export:
# materialize once (one 1Password unlock) into a gitignored file
confit --set stage=development export --profile dev --reveal --out web/py/.env.dev
# or stream it for eval
eval "$(confit export --profile dev --reveal --format shell)"
A profile is just a section under [env], so the other commands work on it too —
confit show env.dev (masked) or confit keys env.dev to list its variables.
composing without a profile
If you don’t want to name a profile, export also accepts multiple sections
directly, merging them left-to-right (later wins on key conflicts):
confit export credentials.app accessories.postgres --reveal --out .env.dev
Profiles are the better home once a composition stabilizes: the env lives in config, version-controlled and shared, instead of being reassembled by hand at every call site.