todo-postgres

July 16, 2026 · View on GitHub

This project was generated by meshc init --template todo-api --db postgres.

This is the PostgreSQL-backed autonomous-cluster starter and the application used by meshc proof docker-autoscaling. PostgreSQL provides shared transactional application state; Mesh owns adaptive execution routing, request continuity, controller consensus, and capacity changes.

Project layout

todo-postgres/
  mesh.toml
  main.mpl
  work.mpl
  config.mpl
  README.md
  Dockerfile
  .dockerignore
  .env.example
  api/
    health.mpl
    router.mpl
    todos.mpl
  deploy/
    todo-postgres.up.sql
  migrations/
    20260402120000_create_todos.mpl
  runtime/
    registry.mpl
  scripts/
    apply-deploy-migrations.sh
    deploy-smoke.sh
    stage-deploy.sh
  services/
    rate_limiter.mpl
  storage/
    todos.mpl
  tests/
    config.test.mpl
  types/
    todo.mpl

Clustered contract

  • mesh.toml carries autonomous scaling, adaptive routing, strict continuity, and Docker capacity-driver policy
  • main.mpl validates config, opens the local PostgreSQL pool, then boots through Node.start_from_env() before starting the local HTTP server
  • work.mpl declares @cluster pub fn sync_todos()
  • the runtime-owned handler name is derived from the ordinary source function name as Work.sync_todos
  • GET /todos and GET /todos/:id use clustered reads; POST /todos uses HTTP.clustered(2, ...) with a caller Idempotency-Key, so either worker can perform the transaction against the same PostgreSQL database and Mesh can safely replay a retained result

HTTP routes

  • GET /health — local runtime + PostgreSQL/starter configuration snapshot without leaking DATABASE_URL
  • GET /todos — list todos through HTTP.clustered(1, ...)
  • GET /todos/:id — fetch one todo through HTTP.clustered(1, ...)
  • POST /todos — clustered idempotent create from {"title":"Buy milk"} with an Idempotency-Key header
  • PUT /todos/:id — toggle completion
  • DELETE /todos/:id — delete a todo

POST /todos is the starter's clustered mutation proof. PUT and DELETE stay local until their callers provide stable operation keys. All mutations go through the actor-backed limiter in services/rate_limiter.mpl.

One public URL, runtime-owned placement

  • BASE_URL in deploy-smoke.sh is the single public app URL for this starter. Locally it can point straight at http://127.0.0.1:8080; behind a proxy/platform ingress it can front multiple starter nodes.
  • Clustered reads and POST /todos responses include a runtime-owned X-Mesh-Continuity-Request-Key response header.
  • Treat that header as an operator/debug seam: take the returned request key and run meshc cluster continuity <node-name@host:port> <request-key> --json against a node when you want the same request's continuity record directly.
  • Use meshc cluster status, meshc cluster continuity, and meshc cluster diagnostics against node addresses when you need the same request's ingress, owner, replica, or execution truth.
  • The response header is a runtime-owned operator/debug seam, not a frontend-aware routing signal.
  • The starter does not promise frontend-aware node selection, sticky-session semantics, or a Fly-specific product contract.
  • If you only need a local-only single-node path, stay on meshc init --template todo-api --db sqlite.

Schema and migrations

migrations/ owns the source-tree schema changes. Apply those migrations before booting the API directly from source:

meshc migrate . status
meshc migrate . up

deploy/todo-postgres.up.sql is the staged bundle artifact for production-like replays. The runtime opens the pool and serves HTTP only after configuration succeeds. It does not create tables at startup.

Environment

  • DATABASE_URL — required PostgreSQL connection string
  • PORT — HTTP port (8080 by default)
  • TODO_RATE_LIMIT_WINDOW_SECONDS — limiter window length (60 by default)
  • TODO_RATE_LIMIT_MAX_REQUESTS — allowed mutating requests per window (5 by default)
  • .env.example — starter copy of the same local-development keys
  • MESH_CLUSTER_COOKIE, MESH_NODE_NAME, MESH_DISCOVERY_SEED, MESH_CLUSTER_PORT, MESH_CONTINUITY_ROLE, MESH_CONTINUITY_PROMOTION_EPOCH — optional clustered-runtime env when you want the runtime-owned operator flow

Local run

meshc migrate . up
meshc build .
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/todo-postgres \
PORT=8080 \
TODO_RATE_LIMIT_WINDOW_SECONDS=60 \
TODO_RATE_LIMIT_MAX_REQUESTS=5 \
./todo-postgres

Staged deploy bundle

Stage a production-like bundle outside the source tree:

bash scripts/stage-deploy.sh ./.bundle/todo-postgres

The staged directory contains:

  • ./.bundle/todo-postgres/todo-postgres — built starter binary
  • ./.bundle/todo-postgres/todo-postgres.up.sql — deploy SQL artifact copied from deploy/
  • ./.bundle/todo-postgres/apply-deploy-migrations.shpsql-based schema apply helper
  • ./.bundle/todo-postgres/deploy-smoke.sh — health + CRUD smoke helper

Each script emits phase-tagged logs: [stage-deploy], [deploy-apply], and [deploy-smoke].

Production-like replay

Apply the staged schema, boot the staged binary, then smoke the running starter through one public BASE_URL:

DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/todo-postgres \
bash ./.bundle/todo-postgres/apply-deploy-migrations.sh ./.bundle/todo-postgres/todo-postgres.up.sql

DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/todo-postgres \
PORT=8080 \
TODO_RATE_LIMIT_WINDOW_SECONDS=60 \
TODO_RATE_LIMIT_MAX_REQUESTS=5 \
MESH_CLUSTER_COOKIE=dev-cookie \
MESH_NODE_NAME=todo-postgres@127.0.0.1:4370 \
MESH_DISCOVERY_SEED=localhost \
MESH_CLUSTER_PORT=4370 \
MESH_CONTINUITY_ROLE=primary \
MESH_CONTINUITY_PROMOTION_EPOCH=0 \
./.bundle/todo-postgres/todo-postgres

The smoke helper treats BASE_URL as the public app URL. In local replay it can point directly at the starter port; behind a proxy/platform ingress it can front multiple nodes without changing the public contract.

DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/todo-postgres \
PORT=8080 \
BASE_URL=http://127.0.0.1:8080 \
bash ./.bundle/todo-postgres/deploy-smoke.sh

The staged bundle is the public deploy contract. Hosted runtimes or managed PostgreSQL providers can sit behind the same bundle later, and a proxy/platform ingress may expose one public app URL in front of multiple nodes, but Mesh runtime ownership still stays on the meshc cluster inspection path and the starter does not promise frontend-aware node selection or a Fly-specific product contract.

Runtime inspection

When you boot the app in clustered mode, inspect the runtime-owned state through Mesh CLI surfaces instead of package-owned control routes:

meshc cluster status <node-name@host:port> --json
meshc cluster continuity <node-name@host:port> --json
meshc cluster continuity <node-name@host:port> <request-key> --json
meshc cluster diagnostics <node-name@host:port> --json

Use the continuity list form first to discover runtime-owned startup records or for general manual investigation.

Docker

Build the Linux binary you want to ship, then package that ./output artifact into the image:

meshc build .
docker build -t todo-postgres .

The container expects a migrated database and a runtime DATABASE_URL; it does not run migrations or create schema at startup. The same staged SQL artifact is the contract for applying schema before the container boots. Re-run meshc build . before rebuilding the image, and build on the Linux target you plan to run because the Dockerfile copies the already-compiled ./output binary into the container.

docker run --rm \
  -p 8080:8080 \
  -e PORT=8080 \
  -e DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/todo-postgres \
  todo-postgres

Docker Compose (clustered)

The docker-compose.yml runs a two-node cluster with a shared PostgreSQL instance. Apply migrations, build the binary, then start the cluster:

meshc build .
docker compose up -d

Note for macOS/Windows users: If you see "exec format error", you need to build for Linux:

# Install Linux target (one-time setup)
rustup target add x86_64-unknown-linux-gnu

# Build for Linux
meshc build --target x86_64-unknown-linux-gnu .

# Then start the cluster
docker compose up -d

The compose file uses a shared DNS alias (mesh-cluster) so every node can discover its peers through MESH_DISCOVERY_SEED=mesh-cluster. Each container's hostname becomes its node identity automatically — no manual MESH_NODE_NAME construction is required. Set MESH_NODE_HOST to the container's reachable hostname or IP when the system hostname is not routable.

Bootstrap environment contract

VariableRequiredDefaultPurpose
MESH_CLUSTER_COOKIEYes (cluster)Shared HMAC-SHA256 secret for node authentication
MESH_DISCOVERY_SEEDYes (cluster)DNS name that resolves to all node IPs
MESH_CLUSTER_PORTNo4370TCP port for inter-node communication
MESH_NODE_NAMENoautoExplicit name@host:port identity (overrides auto)
MESH_NODE_HOSTNohostnameAdvertised address peers use to reach this node
MESH_CONTINUITY_ROLENoprimaryprimary or standby continuity role
MESH_CONTINUITY_PROMOTION_EPOCHNo0Monotonic counter for split-brain prevention
MESH_DISCOVERY_INTERVAL_MSNo5000DNS re-resolution interval in milliseconds

Identity resolution order: MESH_NODE_NAME > FLY_* env > hostname + MESH_NODE_HOST.

Operator inspection

When the cluster is running, inspect it from a node container. Compose already injects the cluster cookie into that container, so the secret is not placed in the command line. The node1 hostname in node1@node1:4370 typically does not resolve from the host machine:

docker compose exec node1 meshc cluster status      node1@node1:4370 --json
docker compose exec node1 meshc cluster continuity  node1@node1:4370 --json
docker compose exec node1 meshc cluster diagnostics node1@node1:4370 --json