Pipeline First
If you haven’t already, re-read localhost:1313/docs/migrate-to-cd/greenfield/, specifically the “Feature Zero” section. This chapter is Feature Zero.
The reading assignment links point to localhost:1313. Make sure your local MinimumCD Practice Guide is running: cd minimumcd-practice-guide && npm start. If you haven’t cloned it yet, see Before You Begin.
You read the greenfield guide. Its core message: the delivery pipeline is the first thing you build. Before the data model, before the UI, before even a “hello world” endpoint.
That’s counterintuitive. You’re excited to build Scimantic. You want to define the knowledge graph schema, write your first Leptos component, see something in a browser. Instead, you’re going to spend this entire chapter on infrastructure: containers, CI/CD, Terraform, security scanning. By the end, you’ll have a URL that returns “ok” and nothing else.
Why? Because every decision you make this chapter is trivial to set up in an empty project and expensive to retrofit later. The greenfield guide is explicit: “Every one of these is trivial to add to an empty project and expensive to retrofit into a mature codebase.” We take that literally.
This chapter has five phases, each ending with a concrete checkpoint where you run something and see it work. At each checkpoint, we’ll revisit the greenfield checklist from Chapter 1 and check off what we completed.
Phases are a Chapter 2 device, not a book-wide convention. This chapter is unusual: it’s “Feature Zero,” sequential infrastructure setup before any user-visible feature ships. The phases give us five natural checkpoints — Podman running, server responding, schema generating, pipeline green, production deployed — that each end with the reader running something and seeing it work.
Starting in Chapter 3, the structure shifts. Chapters 3 through 6 each add a layer of the first vertical slice (database, frontend, end-to-end test, REST API), with one linear flow per chapter. Starting in Chapter 7, every chapter is its own user-story slice (Evidence, Hypotheses, Experiments, Results) and the chapter-internal pacing follows the outside-in TDD pattern from the user stories table in Chapter 1.
If “phase” feels heavy, that’s because it is — it’s load-bearing for this chapter only.
Phase 1: Local Development Environment
Before you write any code, you need a reproducible development environment. “It works on my machine” is the first thing CD eliminates. By the end of this phase, every reader (regardless of operating system) will have identical tooling and an identical PostgreSQL database.
We’ll set up five things:
- An empty GitHub repository for your Scimantic project, cloned locally.
- The Rust toolchain via
rustup, installed directly on your host. - The Scimantic CLI, installed from crates.io.
- A container runtime (Podman) to run services locally.
- A compose file that starts PostgreSQL in a container, matching the version we’ll deploy to production.
Create the Repository
On github.com, click New repository. Name it scimantic (or whatever you prefer), mark it public or private per your preference, and leave everything else unchecked — no README, no .gitignore, no license. We’ll add those deliberately as the chapter progresses, each at the moment we have a reason for it.
Clone it locally:
git clone git@github.com:YOUR-USERNAME/scimantic.git
cd scimantic
The compose.yaml we create shortly will be the first file in this repository. From this point on, every command in the book assumes you’re working inside this directory.
This book expects you to be comfortable with git basics and GitHub. If you need a refresher, see the Before You Begin section for pointers.
The Rust Toolchain
As mentioned in Before You Begin, you are expected to have some basic knowledge of Rust (e.g., you’ve read the first half of The Rust Programming Language), so you likely have Rust installed via the official toolchain manager, rustup. If neither is true, I recommend you go check out The Rust Programming Language) and, in the process, install Rust via rustup. rustup handles multiple Rust versions, lets you pin a project to a specific one, and manages the components we’ll use throughout the book (the stable compiler, rustfmt, clippy, and the WebAssembly target).
After installation, run the following commands in the terminal:
rustc --version
cargo --version
You should see rustc 1.95.0 and cargo 1.95.0 or later (they ship together, so the version numbers always match). Chapter 3 will pin the project’s MSRV (minimum supported Rust version) so the build fails on older toolchains.
We’ll install additional targets and cargo tools (cargo-leptos, sqlx-cli, cargo-nextest, the WebAssembly target, and more) as each chapter needs them.
The Scimantic CLI
With cargo on your path, you can install the Scimantic CLI from crates.io:
cargo install scimantic
Verify it installed correctly:
scimantic --version
You should see scimantic 0.1.x. By the end of the book, you’ll use the scimantic CLI to log into a running Scimantic instance, manage data in your knowledge graph, and query the knowledge graph from the command line.
If you are an “early adopter” of this book, the scimantic CLI will have very minimal functionality (e.g., it prints its version and nothing else). I am dogfooding the CLI as I write the book, and features land chapter by chapter as the REST API gains endpoints.
cargo install downloads the scimantic crate from crates.io, compiles it, and places the binary in ~/.cargo/bin/ (which is on your PATH after installing Rust). This is how Rust tools ship. Later chapters use the same command to install cargo-leptos, sqlx-cli, and other tools.
Why Podman?
You may already have Docker installed. That’s fine; everything in this book works with Docker too. We recommend Podman for three reasons:
- Open source. Podman is Apache-2.0 licensed with no commercial licensing restrictions. Docker Desktop requires a paid subscription for companies with more than 250 employees or $10M+ in annual revenue. Podman has no such restriction.
- Daemonless. The Docker engine runs a persistent background daemon, and the client talks to it over a socket that historically required root. Podman runs containers directly as the invoking user — no daemon, no root by default, smaller attack surface.
- Drop-in compatible.
podman composereads the samecompose.yamlfiles asdocker compose. Every command in this book works with either tool. If you prefer Docker, substitutedockerwherever you seepodman.
Install Podman if you don’t have it according to the Podman Installation Instructions for your operating system.
Also, verify it’s working according to your OS’s instructions. For MacOS, create and start your first Podman machine and verify the installation information:
podman machine init
podman machine start
podman info
The Compose File
Create compose.yaml in the repository root:
# Local development services for Scimantic.
# Matches the production PostgreSQL version (RDS PostgreSQL 16).
#
# Usage:
# podman compose up -d # start services
# podman compose down # stop services
# podman compose down -v # stop and delete data
services:
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: scimantic
POSTGRES_PASSWORD: scimantic
POSTGRES_DB: scimantic
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U scimantic"]
interval: 5s
timeout: 5s
retries: 5
volumes:
pgdata:
A few choices in this file are worth pausing on. Hover over any badge inline with the code above to read why we made that choice — postgres:16 over latest, the explicit healthcheck, and the named pgdata volume.
The username and password (scimantic/scimantic) are for local development only. Production credentials are managed through environment variables and AWS Secrets Manager, never committed to the repository. We’ll set that up in Phase 5.
Start the database:
podman compose up -d
The -d flag runs it in the background. The first time, Podman will pull the PostgreSQL 16 image (~150 MB). Subsequent starts are instant.
Verify it’s running:
podman compose ps
You should see the db service with status Up (and info on how long it’s been up).
Now connect to it:
psql postgres://scimantic:scimantic@localhost:5432/scimantic -c "SELECT version();"
If you don’t have psql installed locally, you can use the one inside the container:
podman compose exec db psql -U scimantic -c "SELECT version();"
You should see PostgreSQL 16.x in the output. That’s the same major version that will run in production on AWS RDS.
Stop the database when you’re done:
podman compose down
Phase 1 Checkpoint
You now have:
- A GitHub repository for your Scimantic project, cloned locally.
- The Rust toolchain (stable) installed on your host.
- The Scimantic CLI installed from crates.io.
- Podman installed and running.
- PostgreSQL 16 running in a container, matching the production version on AWS RDS.
Let’s check off the greenfield checklist items we completed:
- GitHub repository created and cloned locally (done)
- Rust toolchain installed on the host via
rustup(done) - Scimantic CLI installed from crates.io (done)
- Podman is the container runtime (done)
- PostgreSQL runs in a container via
compose.yaml, matching the production version (done)
No code yet, just a reproducible environment where every reader starts from the same place.
Next, we write our first Rust code: a health-check endpoint that proves the server can start.
Phase 2: Hello-World Endpoint + Build
PostgreSQL is running. The toolchain is installed. Now we write the smallest possible Rust web server: a single endpoint at /health that returns the literal string ok. By the end of this phase you’ll be running cargo run, hitting localhost:3000/health from another terminal, and seeing both ok and a structured log line.
This is deliberately tiny. The point is not what it does — it does almost nothing. The point is what’s in place: a Cargo crate with pinned MSRV, an Axum router, a Tokio runtime, structured logging via tracing, and a working build. Every later chapter adds to this skeleton.
The Leptos team ships a cargo leptos new command that scaffolds an entire dual-target (server binary + WASM client) project in one shot. We don’t use it yet. There’s nothing for the WASM client to do until The Web Frontend, and scaffolding a UI we won’t touch for two chapters obscures the small thing we are doing here.
Chapter 4 introduces cargo-leptos at the moment of need, when the first Leptos component lands. The conversion is small: add leptos/leptos_axum/leptos_meta dependencies, add ssr/hydrate features, add the App component. By that point you’ve already used Axum directly and the dual-target build has a reason to exist.
Create the App Crate
From the repository root, create the app/ directory with a single Cargo crate inside:
mkdir -p app/src
The app/ directory is a peer of book/, infra/, and schema/. The repository itself is a monorepo, not a Cargo workspace — app/, book/tools/mdbook-quiz-pdf/, and any other Cargo crates live as independent projects that happen to share a git repo. (Chapter 3 converts app/ itself into a Cargo workspace when the trait-based service layer makes the split worthwhile. For now, app/ is one crate.)
app/Cargo.toml
[package]
name = "scimantic-server"
version = "0.1.0"
edition = "2024"
rust-version = "1.95"
license = "MIT"
description = "Scimantic — scientific knowledge management web application"
repository = "https://github.com/padamson/t2t"
authors = ["Paul Adamson"]
publish = false
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
A few things in this file are worth pausing on. Hover over any badge above to see why we made that choice.
- The crate is named
scimantic-server, notscimantic. The unsuffixedscimanticname on crates.io belongs to the Scimantic CLI you installed in Phase 1; this crate is a different artifact (a deployable web server). The-serversuffix sets up the workspace-split naming for Chapter 3, where we’ll addscimantic-corefor trait definitions. edition = "2024"uses the latest Rust edition. Edition 2024 stabilizes async closures,let-else, and many small ergonomics improvements we’ll use later.publish = falseis a guard against accidentalcargo publish. The web app deploys to AWS, not crates.io.- The dependency set is minimal: Axum (HTTP server), Tokio (async runtime), and
tracingplustracing-subscriber(structured logging). We add more dependencies as each chapter needs them.
app/src/main.rs
use axum::{Router, routing::get};
use tracing::info;
#[tokio::main]
async fn main() {
init_tracing();
let port: u16 = std::env::var("PORT")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(3000);
let addr = format!("0.0.0.0:{port}");
let app = Router::new().route("/health", get(health));
let listener = tokio::net::TcpListener::bind(&addr)
.await
.unwrap_or_else(|e| panic!("failed to bind {addr}: {e}"));
info!(%addr, "scimantic-server starting");
axum::serve(listener, app)
.await
.expect("server crashed");
}
#[tracing::instrument]
async fn health() -> &'static str {
info!("health check");
"ok"
}
fn init_tracing() {
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer())
.init();
}
The whole server is about thirty lines. The callouts walk through the moving parts: how #[tokio::main] boots the async runtime, how Axum’s router maps a path to a handler, how #[tracing::instrument] attaches every log line in health() to a per-request span, and why we read the port from an environment variable.
Every cloud platform — AWS App Runner, Fly.io, Heroku, Cloud Run — sets a PORT environment variable on the container telling the app which port to bind. Hard-coding 3000 works locally; reading PORT works locally and in production. The default of 3000 matches the cargo-leptos convention we’ll adopt in Ch 4, so when the time comes the local URL doesn’t change.
.gitignore: ignore the target/ directory
Cargo writes build artifacts to a target/ directory beside the Cargo.toml. In t2t the file already ignores build/ (mdbook output) and a few other things. Add Rust build artifacts to the rule:
target/
The bare target/ (no path prefix) matches at any depth — app/target/, book/tools/mdbook-quiz-pdf/target/, and any future Cargo crate’s build directory. One rule, all crates covered.
Run It
From app/:
cargo run
The first build pulls Axum, Tokio, and tracing from crates.io and compiles them; subsequent runs are quick. Once you see scimantic-server starting, leave it running and from another terminal:
curl http://localhost:3000/health
# ok
Watch the server terminal: you should see a health: health check log line. The health: prefix is the span name from #[tracing::instrument]; everything inside that handler now reports under that span, which is what makes a structured-logging system useful when you’re staring at a thousand requests at once.
If you have mdbook serve running for the book, it’s holding port 3000. Either stop it, run mdbook serve --port 4000 to move it aside, or run the server on a different port: PORT=4000 cargo run and curl localhost:4000/health instead.
Stop the server with Ctrl-C.
Phase 2 Checkpoint
You now have:
- A Cargo crate at
app/namedscimantic-server, with MSRV 1.95, the Axum + Tokio +tracingdependency set, project-wide lints turned on, andpublish = falseblocking accidental release. - A working Axum server with a
/healthendpoint that returnsok. - Structured logging via
tracingshowing both startup info and per-request spans. target/gitignored so build artifacts don’t pollute the repo.
Greenfield checklist items completed:
- First Rust crate (
scimantic-server) created atapp/, MSRV pinned (done) - Axum server builds and runs locally (done)
- Structured logging with
tracingfrom the first handler (done)
What we deliberately haven’t done yet — saved for the right chapter:
- CI workflow. Phase 4 wires
app/into GitHub Actions alongside the security and supply-chain checks. - Pre-commit hooks. Same — Phase 4.
cargo-leptosand a UI. Chapter 4.- Workspace structure. Chapter 3, when the service layer trait-ports earn the split.
Next: author the local LinkML schema for User, declare it in panschema.toml, and run panschema fetch && panschema generate to produce the Rust types you’ll consume in Chapter 3.