Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Pipeline First

Reading Assignment

If you haven’t already, re-read localhost:1313/docs/migrate-to-cd/greenfield/, specifically the “Feature Zero” section. This chapter is Feature Zero.

Practice Guide Running?

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.

Why phases here, but not later?

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:

  1. An empty GitHub repository for your Scimantic project, cloned locally.
  2. The Rust toolchain via rustup, installed directly on your host.
  3. The Scimantic CLI, installed from crates.io.
  4. A container runtime (Podman) to run services locally.
  5. 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.

Info

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.

Info

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.

Info

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:

  1. 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.
  2. 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.
  3. Drop-in compatible. podman compose reads the same compose.yaml files as docker compose. Every command in this book works with either tool. If you prefer Docker, substitute docker wherever you see podman.

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.

Warning

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();"

Tip

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.

Why bare Axum, not cargo-leptos?

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, not scimantic. The unsuffixed scimantic name on crates.io belongs to the Scimantic CLI you installed in Phase 1; this crate is a different artifact (a deployable web server). The -server suffix sets up the workspace-split naming for Chapter 3, where we’ll add scimantic-core for 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 = false is a guard against accidental cargo publish. The web app deploys to AWS, not crates.io.
  • The dependency set is minimal: Axum (HTTP server), Tokio (async runtime), and tracing plus tracing-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.

Why PORT from the environment?

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.

Port 3000 conflicts with mdbook serve

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/ named scimantic-server, with MSRV 1.95, the Axum + Tokio + tracing dependency set, project-wide lints turned on, and publish = false blocking accidental release.
  • A working Axum server with a /health endpoint that returns ok.
  • Structured logging via tracing showing 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 at app/, MSRV pinned (done)
  • Axum server builds and runs locally (done)
  • Structured logging with tracing from 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-leptos and 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.