Customer Project Layout
This page shows the exact workspace shape the tutorial is using and what each file contributes.
The Workspace Shape
tutorial-app/
Cargo.toml
app.toml
platform.dev.toml
docker-compose.yml
crates/
tutorial-app-app/
Cargo.toml
src/lib.rs
tutorial-app-backend/
Cargo.toml
src/lib.rs
tutorial-app-bin/
Cargo.toml
src/main.rs
templates/
layouts/
pages/
components/
theme/
assets/
auth/
tutorial-auth/
This split matters because the tutorial is teaching a customer-owned app, not a monolithic starter crate.
Root Cargo.toml
[workspace]
members = [
"crates/tutorial-app-app",
"crates/tutorial-app-backend",
"crates/tutorial-app-bin",
]
resolver = "2"
[workspace.package]
edition = "2021"
version = "0.1.0"
[workspace.dependencies]
coil-all = "0.1"
coil-customer-sdk = "0.1"
serde = { version = "1", features = ["derive"] }
This file does two things:
- it makes the three customer crates build together
- it centralizes shared dependency versions
crates/tutorial-app-bin/Cargo.toml
[package]
name = "tutorial-app-bin"
version.workspace = true
edition.workspace = true
[dependencies]
tutorial-app-app = { path = "../tutorial-app-app" }
This crate depends only on the app crate because it is just the process entrypoint.
crates/tutorial-app-app/Cargo.toml
[package]
name = "tutorial-app-app"
version.workspace = true
edition.workspace = true
[dependencies]
coil-all.workspace = true
coil-config = "0.1"
tutorial-app-backend = { path = "../tutorial-app-backend" }
This crate depends on Coil plus the customer backend because it composes the runtime.
crates/tutorial-app-backend/Cargo.toml
[package]
name = "tutorial-app-backend"
version.workspace = true
edition.workspace = true
[dependencies]
coil-customer-sdk.workspace = true
This crate depends only on the stable customer SDK because it should not need runtime internals.
app.toml
name = "tutorial-app"
display_name = "Tutorial App"
[domains]
canonical = "www.127.0.0.1.nip.io"
additional = []
[i18n]
default_locale = "en-GB"
supported_locales = ["en-GB"]
[theme]
asset_roots = ["theme/assets"]
[auth]
package = "tutorial-auth"
[[modules]]
name = "admin"
[[modules]]
name = "cms"
[[modules]]
name = "commerce"
This file defines product structure:
- product identity
- public domains
- locales
- enabled official modules
- theme and auth roots
platform.dev.toml
[app]
name = "tutorial-app"
environment = "development"
[server]
bind = "127.0.0.1:8080"
[database]
mode = "postgres"
url = "postgres://postgres:[email protected]:5432/tutorial_app"
[cache]
mode = "redis"
url = "redis://127.0.0.1:6379"
[storage]
mode = "local"
local_root = ".coil/state"
This file defines how the app runs in development:
- which port to bind
- where database and cache backends live
- where local state is stored
docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: tutorial_app
ports:
- "5432:5432"
redis:
image: redis:7
ports:
- "6379:6379"
This file starts the local services the platform config points at.
crates/tutorial-app-app/src/lib.rs
use coil_all::modules;
use coil_config::PlatformConfig;
pub fn run_from_args(
args: impl IntoIterator<Item = String>,
) -> Result<(), Box<dyn std::error::Error>> {
let mut args = args.into_iter();
let _program = args.next();
match args.next().as_deref() {
Some("validate") => {
let _config = PlatformConfig::from_file("platform.dev.toml")?;
Ok(())
}
Some("serve") => {
coil_all::builder()
.with_customer_plugin(tutorial_app_backend::TutorialAppPlugin)
.with_module(modules::admin())
.with_module(modules::cms())
.with_module(modules::commerce())
.run_from_env()?;
Ok(())
}
other => Err(format!("unknown command: {:?}", other).into()),
}
}
This file enables the app’s runtime behavior:
- loads config during validation
- links the customer backend
- registers official modules
- serves through the app-owned composition root
crates/tutorial-app-bin/src/main.rs
use std::process::ExitCode;
fn main() -> ExitCode {
match tutorial_app_app::run_from_args(std::env::args()) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}
This file makes the app runnable as a normal binary.
crates/tutorial-app-backend/src/lib.rs
use coil_customer_sdk::{CustomerBackendPlugin, CustomerHookRegistry};
pub struct TutorialAppPlugin;
impl CustomerBackendPlugin for TutorialAppPlugin {
fn register(
&self,
_registry: &mut dyn CustomerHookRegistry,
) -> Result<(), coil_customer_sdk::BackendError> {
Ok(())
}
}
This file is the starting point for customer-owned backend rules.
Checkpoint
Run:
docker compose up -d
cargo run -p tutorial-app-bin -- validate
cargo run -p tutorial-app-bin -- serve
At this point you should be able to explain which file owns product structure, runtime config, composition, process entry, and customer backend behavior.