Project Organization
Coil works best when the customer app, the runtime configuration, and any optional extension lanes are structurally separate.
This is not just source-tree aesthetics. It is the operational boundary that keeps deploys, troubleshooting, and ownership clear.
What Is This?
This page explains how to lay out a serious Coil codebase so that:
- the customer binary is obvious
- product and operational inputs are not mixed together
- linked customer Rust and bounded extensions stay distinct
- release inputs are easy to identify
Why Does This Matter?
Bad structure causes predictable operational failures:
- startup and composition logic become hard to trace
- app identity and runtime topology get mixed together
- customer-owned code starts looking like an undocumented plugin system
- deploy and rollback steps stop matching the repository
Good structure is one of the cheapest ways to keep a Coil app operable.
The Canonical Shape
A healthy Coil customer project usually has four clear areas:
- a Rust workspace that owns the customer binary and customer-owned crates
- an app root that owns
app.toml, templates, theme assets, auth files, and extensions - per-environment runtime config such as
platform.dev.tomlandplatform.toml - deployment tooling such as container files and scripts
That shape matters more than the exact folder names.
A Practical Layout
This is a good starting point:
customer-product/
Cargo.toml
crates/
product-bin/
product-app/
product-backend/
app.toml
platform.dev.toml
platform.toml
auth/
templates/
theme/
extensions/
docker-compose.yml
Dockerfile
What each area is for:
product-bin/: the composition root and operator-facing binaryproduct-app/: app/bootstrap logic and customer-specific runtime assemblyproduct-backend/: linked customer Rust behaviourapp.toml: product composition and app identityplatform*.toml: environment-specific runtime topologyauth/: customer auth package filestemplates/andtheme/: frontend presentationextensions/: bounded runtime-installed packages
What Belongs Where
Put this in the Rust workspace
- binary composition
- customer-specific backend logic
- any shared customer support crates
Put this in the app root
- app identity
- site and locale structure
- enabled modules
- templates and theme assets
- auth package files
- extension package declarations
Put this in runtime config
- bind addresses
- database, cache, jobs, and storage backends
- TLS mode
- observability settings
- CDN and asset delivery settings
Put this in secret storage
- database URLs
- provider API keys
- webhook secrets
- object-store credentials
When To Use coil
Use coil when:
- you want the default official battery while learning
- you want the shortest path to a coherent full stack
- you are comfortable letting the app manifest decide which linked modules are actually enabled
Use narrower selective dependencies when:
- you are building a specialized product shape
- you want tighter control over what the binary links
- you need a very explicit runtime surface for review or compliance
Even when coil is used, the customer binary still owns composition.
How To Add A New Customer Crate
When adding a crate to a customer workspace:
- decide whether it is binary composition, app/bootstrap logic, customer backend logic, or shared support code
- add it to the customer workspace manifest
- link it intentionally from the customer binary or app crate
- keep its responsibility narrow enough that another operator can tell why it exists
Good rule:
- if the crate owns product-specific behaviour, it belongs in customer code
- if the crate is becoming generic enough for many apps, reevaluate whether it wants to be an official module instead
How To Add A Backend Crate
If you need customer-specific backend behaviour:
- create a dedicated backend crate
- implement supported customer SDK hooks or facades there
- register it from the customer binary or app composition crate
- keep it separate from lower-trust runtime-installed extensions
If the code genuinely needs its own process boundary, use a sidecar or external integration on purpose. Do not create a fake service boundary by habit.
How To Add An Extension Directory
If you need bounded runtime-installed extensions:
- create an
extensions/area - keep extension code separate from linked customer Rust
- declare and pin the extension from the app manifest
- treat it as a replaceable, lower-trust boundary
The important distinction is:
- linked customer Rust is part of the app
- runtime-installed extensions are explicitly bounded add-ons
Multi-Site Project Organization
For multi-site products, do not clone the application three times unless there is a real product boundary that requires it.
The Coil shape is:
- one customer workspace
- one app manifest
- one deployment surface
- site-specific behaviour expressed in config and content
That keeps rollout, auth, jobs, and operations coherent.
Supporting Repo Examples
If you want concrete examples after reading the pattern:
- Shoppr shows the multi-site commerce shape
- Gitly shows the single-site non-commerce shape
Those examples are useful because they prove the model, but they are not the primary teaching material for this page.
Common Mistakes
Mixing product and operational inputs
app.toml and platform.toml should not become one ambiguous blob of settings.
Hiding composition in scripts
If the only way to understand startup is to read shell wrappers, the customer binary is too opaque.
Treating linked Rust and extensions as one lane
They exist for different trust and ownership models.
Letting local maintainer overrides become the public contract
Repo-maintainer conveniences are useful, but they are not the supported customer deployment model.