Configuration And Secrets
Coil separates customer composition, operational configuration, and secrets on purpose.
That split is one of the platform's core safety properties.
What Is This?
This page explains:
- what belongs in the app manifest
- what belongs in
platform.dev.tomlandplatform.toml - what belongs in secret storage or environment variables
- how to reason about a complete configuration set
Why Does This Separation Exist?
Without it, teams usually end up with one giant config surface that mixes:
- product shape
- environment topology
- credentials
- temporary operational workarounds
That makes review harder, drift easier, and incident recovery slower.
The Three Configuration Inputs
1. Customer app manifest
The app manifest describes product composition:
- app identity
- enabled modules
- site and locale structure
- theme settings
- auth package identity
- runtime-installed extension declarations
2. Platform runtime config
Platform config describes how the product is operated in a specific environment:
- bind address
- database, cache, jobs, and storage backends
- TLS mode
- observability settings
- asset delivery settings
- module-specific operational config
3. Secrets
Secrets provide sensitive runtime values:
- database URLs
- object-store credentials
- payment API keys
- webhook secrets
- TLS provider credentials
A Canonical Coil Example
This is the mental model you should start from:
[database]
url = { kind = "env", var = "DATABASE_URL" }
schema = "public"
[cache]
l1 = "moka"
l2 = "redis"
[jobs]
backend = "redis"
[observability]
metrics = true
tracing = true
[assets]
publish_manifest = true
cdn_base_url = "https://cdn.example.com"
What this example demonstrates:
- topology lives in platform config
- sensitive values resolve from environment or secret storage
- product structure does not live here unless it is truly operational
How To Think About The Important Blocks
[server]
Use it for:
- bind address
- trusted proxies
Do not use it for product identity, site catalog policy, or theme behaviour.
[database]
Use it for:
- connection URL reference
- schema
- pool sizing
- statement timeouts
The secret value for the URL should still come from a secret source.
[storage]
Use it for:
- object store type
- local root
- deployment mode
- secret reference for credentials
[cache]
Use it for:
l1in-process cachel2shared cache such as Redis or Valkey
This is an operational performance choice, not an app-manifest concern.
[i18n] and [[sites]]
Use them for:
- default and supported locales
- route localisation policy
- host and canonical host mapping
These are runtime-facing because they affect routing and delivery, even though they are also part of product behaviour.
[auth]
Use it for:
- auth package identity
- auth backend wiring such as tenant id where applicable
Do not place app-specific capability semantics here if they belong in auth package files.
[modules]
Use it for:
- enabled linked official modules
- module-specific operational configuration
Example:
[modules."commerce-payments-stripe"]
provider = "stripe"
checkout_mode = "hosted-checkout"
publishable_key = { kind = "env", var = "STRIPE_PUBLISHABLE_KEY" }
webhook_secret = { kind = "env", var = "STRIPE_WEBHOOK_SECRET" }
[wasm]
Use it for:
- extension artifact directory
- runtime limits
- secret bindings exposed to extensions
[jobs]
Use it for:
- queue backend selection
[observability]
Use it for:
- metrics
- tracing
[assets]
Use it for:
- whether publication emits an asset manifest
- the asset origin or CDN base URL
Development Versus Production
Development and production do not need identical infrastructure, but they should preserve the same behavioural model.
Good differences:
secure = falsefor dev cookies andtruefor production- local object-store/CDN URLs in development and real ones in production
- local
tls.mode = "external"in development and real TLS automation in production
Bad differences:
- different module sets
- different site topology
- different auth package identity
- different route graph
A Practical Local Secret Example
For local development, a small .env can be enough:
DATABASE_URL=postgres://coil:[email protected]:5432/coil_app
OBJECT_STORE_URL=...
STRIPE_PUBLISHABLE_KEY=pk_test_replace_me
STRIPE_SECRET_KEY=sk_test_replace_me
STRIPE_WEBHOOK_SECRET=whsec_replace_me
The exact variable names vary by app and provider, but the rule stays the same:
- committed config references the secret
- the secret value arrives from the environment
Secrets Handling Rules
Prefer:
- environment variables
- deployment-time secret injection
- a secret manager exposed through the runtime environment
Avoid:
- committing provider keys
- copying production secrets into local config files
- embedding secrets in templates or frontend assets
Supporting Repo Examples
After you understand the pattern, the checked-in apps are useful supporting examples:
- Shoppr shows payment-provider and webhook-heavy config
- Gitly shows a simpler non-commerce config shape
Use those examples as proofs of the model, not as the primary teaching material.
Common Mistakes
Putting product behaviour in platform.toml
Product composition belongs in the app manifest unless it is genuinely an operational concern.
Putting credentials in committed config
Secrets belong in secret resolution, not in versioned product files.
Making development config too fake
Development can be lighter than production, but it should still exercise the same integration boundaries.