platform.toml And platform.dev.toml
platform.toml and platform.dev.toml are runtime configuration files.
They describe:
- environment and server policy
- HTTP session, flash, and CSRF behaviour
- TLS mode
- database, storage, cache, jobs, and observability
- site host bindings at runtime
- auth runtime settings
- official module runtime settings
- WASM loading policy
- asset publication settings
platform.dev.toml is usually a development-safe variant of the same schema. It is not a different model.
Why These Files Exist
Coil keeps runtime operations separate from product composition.
app.tomlsays what the app isplatform.tomlsays how it runs in one environment
That lets the same customer app move across local development, staging, and production without rewriting the app manifest.
In practice:
app.tomlchanges when the product changesplatform.dev.tomlchanges when your local environment changesplatform.tomlchanges when your production infrastructure or operating policy changes
Shoppr As The Working Example
The checked-in Shoppr platform config files are the best current concrete examples:
apps/shoppr/platform.dev.tomlapps/shoppr/platform.toml
Keep those files open while reading this page. They show the same product running with two different runtime policies:
- local development on plain HTTP with development-safe cookies and a local CDN/object-store URL
- production-oriented TLS, secure cookies, a production local root, and a production CDN base URL
One Schema, Two Common Files
Typical use:
platform.toml: production-oriented defaultsplatform.dev.toml: local or developer-safe overrides
Examples of normal differences:
- cookie
secure = falsein development tls.mode = "external"locally- local CDN/object-store endpoints
- development-friendly asset URLs
The point is not to maintain two unrelated files. The point is to keep one runtime schema and two environment-shaped realizations of it.
Top-Level Sections
The current platform config loader supports:
[app][server][http.session][http.session_cookie][http.flash_cookie][http.csrf][tls][database][storage][cache][i18n][seo][[sites]][auth][modules][wasm][wasm.secret_bindings][[wasm.outbound_http]][jobs][observability][assets]
Not every app needs every block, but most production apps will use most of them.
Reference Example
[app]
name = "shoppr"
environment = "production"
[server]
bind = "0.0.0.0:8080"
trusted_proxies = ["10.0.0.0/8"]
[http.session]
store = "redis"
idle_timeout_secs = 3600
absolute_timeout_secs = 86400
[http.session_cookie]
name = "coil_session"
path = "/"
same_site = "lax"
secure = true
http_only = true
[http.flash_cookie]
name = "coil_flash"
path = "/"
same_site = "lax"
secure = true
http_only = true
[http.csrf]
enabled = true
field_name = "_csrf"
header_name = "x-csrf-token"
[tls]
mode = "acme"
challenge = "dns-01"
provider = "cloudflare-dns"
[database]
url = { kind = "env", var = "DATABASE_URL" }
schema = "public"
[storage]
default_class = "public_upload"
deployment = "distributed"
object_store = "s3"
object_store_secret = { kind = "env", var = "OBJECT_STORE_URL" }
local_root = "/var/lib/coil/shoppr"
[cache]
l1 = "moka"
l2 = "redis"
[auth]
package = "shoppr-auth"
explain_api = false
tenant_id = 101
A Practical Reading Strategy
Read platform config in this order:
- What environment am I in?
- How do requests enter the runtime?
- How are sessions and CSRF enforced?
- How does the runtime reach data, storage, cache, and jobs?
- How is TLS handled?
- How are sites and canonical hosts represented at runtime?
- Which module-specific settings exist?
- Where are assets published and served from?
That is usually the same order you debug it in too.
[app]
Supported keys:
nameenvironment
environment values:
developmentstagingproduction
What This Block Means
This is the runtime identity and environment mode.
name should match the app manifest. environment changes how the runtime interprets safety-sensitive behaviour such as local HTTP object-store access and other development allowances.
Example
[app]
name = "shoppr"
environment = "development"
Guidance
developmentshould be the normal local modeproductionshould be used only when the surrounding infra is production-shaped- do not use production mode locally unless you deliberately want production-like restrictions
[server]
Supported keys:
bindtrusted_proxiesmax_body_bytes
What This Block Means
This is the transport-edge block for the HTTP server. It controls:
- where the server listens
- which upstream proxies are trusted to supply forwarded metadata
- request body size limits
Example
[server]
bind = "0.0.0.0:8080"
trusted_proxies = ["10.0.0.0/8"]
max_body_bytes = 10485760
Guidance
- use
bindfor local or container network binding, not for public hostname modelling - use
trusted_proxiesonly for real proxy networks you control - keep body limits explicit for upload-heavy apps
[http.*]
HTTP config is split into four typed sections.
[http.session]
Supported keys:
storeidle_timeout_secsabsolute_timeout_secs
Supported store values:
memorydatabaseredisvalkey
What This Block Means
This controls how browser sessions are persisted and how long they remain valid.
Example
[http.session]
store = "redis"
idle_timeout_secs = 3600
absolute_timeout_secs = 86400
Guidance
memoryis only for local or explicitly single-node usedatabase,redis, orvalkeyare the real shared-store optionsidle_timeout_secsis inactivity-basedabsolute_timeout_secsis hard-stop lifetime
[http.session_cookie] and [http.flash_cookie]
Supported keys:
namedomainpathsame_sitesecurehttp_onlyprotection
Supported same_site values:
laxstrictnone
Supported protection values:
signedencrypted
What These Blocks Mean
These blocks define the cookie transport behaviour for sessions and flash state.
Example
[http.session_cookie]
name = "coil_session"
path = "/"
same_site = "lax"
secure = true
http_only = true
protection = "encrypted"
Guidance
secure = falseis normal in local plain-HTTP developmentsecure = trueshould be the production default- use
encryptedwhen the cookie value should not be readable client-side - keep cookie naming stable across deployments unless you are deliberately rotating transport state
[http.csrf]
Supported keys:
enabledfield_nameheader_name
What This Block Means
This enables and names CSRF transport channels for state-changing browser requests.
Example
[http.csrf]
enabled = true
field_name = "_csrf"
header_name = "x-csrf-token"
Guidance
- leave it enabled for normal browser apps
- use the documented field and header names in forms and enhanced requests
[tls]
Supported keys:
modechallengeprovideraccount_secret
Supported mode values:
externalacmecloudflare-originmanual
Supported challenge values:
http-01tls-alpn-01dns-01
Supported provider values:
cloudflare-dnscloudflare-origin-camanual-import
What This Block Means
This is the TLS ownership and issuance block. It tells Coil whether:
- TLS is handled outside Coil
- Coil should obtain certificates
- Coil should validate origin-only certificates
- certificates are manually imported
Guidance
- use
externalin local development or when TLS is terminated elsewhere - use
acmefor normal public certificate management - use
cloudflare-originonly when the origin is intentionally private behind Cloudflare - use
dns-01for wildcard-heavy or CDN-fronted deployments
[database]
Supported keys:
driverurlschemamigrations_tablemin_connectionsmax_connectionsstatement_timeout_secs
Supported driver values:
postgres
The URL is a SecretRef, typically:
url = { kind = "env", var = "DATABASE_URL" }
What This Block Means
This is the runtime database connection contract for:
- application data
- migration ownership
- shared runtime surfaces that rely on database connectivity
Guidance
- Postgres is the production-grade path
- set connection pool sizes deliberately
- keep
migrations_tablestable once a deployment is in use - use
statement_timeout_secsto bound runaway queries
[storage]
Supported keys:
default_classdeploymentsingle_node_escape_hatchobject_storelocal_rootobject_store_secret
Supported default_class values:
public_assetpublic_uploadprivate_sharedlocal_only_sensitive
Supported deployment values:
distributedsingle_node
Supported single_node_escape_hatch values:
disabledexplicit_single_node
Supported object_store values:
s3
What This Block Means
This block defines the storage topology and delivery posture for assets and uploads.
Guidance
- use
distributedwhen the app is meant to scale beyond one node - treat
single_node_escape_hatchas an explicit exception, not a default local_rootstill matters even in distributed setups because some local-only classes remain intentionally localobject_store_secretshould resolve to the structured secret shape Coil expects
[cache]
Supported keys:
l1l2
Supported l1 values:
moka
Supported l2 values:
redisvalkey
What This Block Means
Coil uses a two-level cache vocabulary:
l1: in-process cache close to the runtime instancel2: shared cache across instances
Guidance
l1is the fast local layerl2is the shared coordination layer- a distributed production deployment should normally have both
- a small local development setup can still feel fine with the same shape because Shoppr uses
mokaplusredis
If you are asking "which one is required?", the real answer is:
- single-process local development can survive with less
- production distributed deployments should treat
l2as part of the real contract
[i18n] and [seo]
These sections provide app-wide runtime defaults and compatibility sugar.
[i18n]
Supported keys:
default_localesupported_localesfallback_localelocalized_routes
[seo]
Supported keys:
canonical_hostemit_json_ld
Guidance
These blocks should align with the product contract expressed in app.toml. They are runtime defaults and validation context, not a replacement for the app manifest.
[[sites]]
Supported keys:
iddisplay_namebrand_namecanonical_hosthostsdefault_localesupported_locales
What This Block Means
This is the runtime host-resolution view of multi-site configuration.
Use it to tell the runtime:
- which hostnames map to which site
- which locale defaults apply at runtime
- which brand display strings should be surfaced per site
Guidance
- this block should stay aligned with
app.toml - runtime hostnames here are operational host bindings, not just product declarations
- keep host coverage explicit; unknown hosts should fail closed
[auth]
Supported keys:
packageexplain_apitenant_id
What This Block Means
This is the runtime binding for the selected auth package and related auth runtime behaviour.
Guidance
packageshould match the app manifest selectiontenant_idshould be stable per deploymentexplain_apishould only be enabled deliberately when you want that operational surface available
[modules]
[modules] mirrors app-level intent but allows module-specific runtime config blocks.
Example
[modules]
enabled = ["commerce", "commerce-payments-stripe"]
[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" }
Guidance
Think of this as the runtime wiring layer for installed modules. The app manifest chooses the product battery; platform config supplies runtime secrets and provider settings for that battery.
[wasm], [wasm.secret_bindings], and [[wasm.outbound_http]]
These sections control the runtime-only extension host.
[wasm] supported keys
directorydefault_time_limit_msallow_network
[wasm.secret_bindings]
Maps named extension-visible secret bindings to platform secrets.
[[wasm.outbound_http]]
Declares explicitly allowed outbound HTTP endpoints for extensions.
Guidance
- keep the extension directory explicit
- prefer deny-by-default for network access
- make secret bindings narrow and named
- use explicit endpoint mappings instead of ambient outbound access
[jobs]
Supported keys:
backend
Typical values include:
redisvalkey
What This Block Means
This selects the shared backend used for queues, leases, scheduled work, and recovery operations.
Guidance
Use the same shared backend story as the rest of the deployment. If the app is distributed, jobs should be too.
[observability]
Supported keys:
metricstracing
What This Block Means
This toggles baseline runtime observability surfaces.
Guidance
These are not abstract nice-to-haves. They are the runtime switches that determine whether the platform emits the signals the operations docs rely on.
Read next:
[assets]
Supported keys:
publish_manifestcdn_base_url
What This Block Means
This block controls how published theme assets are surfaced after publication.
Guidance
cdn_base_url is not inherently required in the abstract, but if publish_manifest is enabled the runtime needs a stable delivery base to point published asset URLs at.
That base can be:
- a true CDN domain
- an object-store-backed delivery domain
- a same-domain asset host if your production topology is intentionally set up that way
The important thing is not "must this be a CDN?" The important thing is "is there a stable, production-valid base URL for published assets?"
Shoppr demonstrates both:
- local development using
http://localhost:9000/shoppr - production using
https://cdn.example.com
Common Configuration Decisions
Can I serve production assets from my main site instead of a CDN?
Yes, if your production topology is intentionally built that way and the delivery URL is stable. Coil does not require a third-party CDN brand name. It requires a reliable published-asset base URL.
Should I use both l1 and l2 cache?
For distributed production systems, yes. l1 gives process-local speed; l2 gives cross-instance coordination.
Should local development mirror production shape?
Broadly yes, but with development-safe differences:
- plain HTTP
- insecure cookies if needed locally
- local delivery endpoints
- externally terminated TLS mode
Common Mistakes
- Putting product configuration into platform config instead of
app.toml - Running production mode locally and then treating local failures as product problems
- Treating
trusted_proxiesas a convenience wildcard - Using in-memory sessions for a deployment that expects shared state
- Forgetting that module runtime blocks are separate from app manifest module enablement
- Treating
cdn_base_urlas a branding choice instead of a delivery contract