cli.md

July 17, 2026 · View on GitHub

The ddd CLI is the fastest way to start a ddd_cqrs_es application and keep generated projects consistent as they grow.

It is published by the ddd-cqrs-es-cli crate, installs a binary named ddd, and is released with the same version as ddd_cqrs_es.

Use it for:

  • new app scaffolding with opinionated presets
  • fine-grained additions like events, commands, routes, projections, and tests
  • capability wiring for Redis, gRPC, tracing, snapshots, and idempotency
  • Spin runtime command resolution
  • agent/MCP workflows that need deterministic dry-run JSON before writing files

Install

Install the published CLI:

cargo install ddd-cqrs-es-cli --locked

Check that it is available:

ddd --help
ddd capabilities --json

When developing this repository locally, use the workspace binary instead:

cargo run -p ddd-cqrs-es-cli -- <args>

For example:

cargo run -p ddd-cqrs-es-cli -- init billing --preset basic --domain Invoice

Quick Start

Scaffold the production Spin + Leptos + wasi-auth template (same tree as examples/fullstack-app):

ddd init my-saas --preset fullstack
# or from this monorepo: make scaffold-fullstack DIR=my-saas
cd my-saas
cp .env.example .env
make db-up
make dev transport=both
# open http://localhost:3008

ddd serve on a fullstack project resolves to make dev transport=both (Spin plus the outbox worker so verification mail can deliver). Use make spin only when you intentionally want the app without mail delivery.

What works after fullstack init

CommandFullstack
ddd checkYes — validates product file inventory
ddd serve / ddd watchYes — make dev (+ cargo-watch for watch)
ddd freshYes — make db=postgres fresh (Postgres must be up)
ddd enable auth / oauth-provider / passkeysManifest bookkeeping only; still set Spin/.env secrets
ddd add aggregateYes — src/domain + src/domain_app (InMemory demo store) + /api/domain/... REST + lib/rest hooks
ddd add event / ddd add commandYes — once an aggregate exists (serde/command markers)
ddd add projection|route|server-fn|…No — refuses unwired product stubs

Product domain aggregates live beside the wasi-auth shell under src/domain/. ddd add aggregate also wires a demo application service and REST under /api/domain/{module}/… (process-local InMemoryEventStore — swap for durable storage before production; routes are not Cedar-gated by default). Stock examples/fullstack-app stays domain-free; dual-sync excludes src/domain/, src/domain_app/, and src/domain_rest.rs.

Domain-only or thin Leptos app

Create a small domain-only project:

ddd init billing --preset basic --domain Invoice
cd billing
ddd check

Add a domain event:

ddd add event Invoice InvoicePaid --field amount:i64 --field paid_at:String

Add a command:

ddd add command Invoice PayInvoice --field amount:i64

Preview any write before applying it:

ddd --dry-run --format json add event Invoice PaymentFailed --field reason:String

The dry-run output reports the planned file operations without changing files.

Command Model

Every command supports the same global controls:

OptionPurpose
--cwd <path>Run against a generated project without changing the shell directory.
--dry-runPreview file operations or runtime command resolution without writing or executing.
--format text|jsonChoose human text or machine-readable JSON output.
--forceAllow overwriting files that would otherwise be protected. Inspect collisions first.
--yesReserved for non-interactive confirmations as workflows grow.

The main command groups are:

CommandUse
ddd init <path>Create a generated project and ddd.toml manifest.
ddd add ...Add aggregates, events, commands, routes, tests, projections, and related stubs.
ddd enable ...Wire capabilities into an existing generated project.
ddd serveResolve and run the app through the selected Spin runtime command.
ddd watchResolve a rebuild/restart loop for the selected runtime command.
ddd freshReset schema/data only. This does not start the server.
ddd doctorInspect required local tools.
ddd checkValidate the generated project manifest and required files.
ddd matrixPrint supported backend/realtime/transport combinations.
ddd capabilities --jsonExpose the machine-readable CLI contract for agents and MCP tools.

Presets

Choose a preset with ddd init --preset <preset>.

PresetBest ForDefault Shape
basicLearning the framework or building a pure domain crate.Aggregate, command, event, fixture test, and in-memory example.
leptos-wasiFull-stack app scaffolding for Leptos WASI on Spin.Domain/application/store/server boundaries with REST/SSE and optional gRPC.
fullstackProduction-oriented Spin fullstack application.Leptos islands, complete account/organization/admin pages, trusted auth, embedded Cedar, REST, and unary plus streaming gRPC.
native-apiNative Rust API service shape.Axum-style API scaffold with native SQL adapter features.
workerProjection or process-manager workers.Worker entrypoint plus projection/process-manager-oriented stubs.
customA minimal base for explicit, agent-chosen capabilities.Starts from the basic shape and lets you add capabilities intentionally.

Examples:

ddd init billing --preset basic --domain Invoice
ddd init counter-app --preset leptos-wasi --domain Counter --db sqlite --runtime spin --transport http --ui leptos
ddd init counter-grpc --preset leptos-wasi --domain Counter --db postgres --runtime spin --transport both --ui leptos
ddd init fullstack --preset fullstack --runtime spin --db postgres --transport both --ui leptos
ddd init projector --preset worker --domain Invoice --db mysql --realtime polling

Current CLI-generated apps are Spin-focused. The runtime value is spin. The fullstack preset is intentionally fullstack; it defaults to transport=both and ui=leptos, and rejects narrower transport/UI shapes. Generated fullstack projects include the same Leptos, REST, gRPC, storage, OAuth, passkey, organization, administration, audit-stream, smoke-test, and rollout surface as the reference examples/fullstack-app app. They also include spin.production.toml.example with exact OAuth/database outbound hosts to use as the production hardening starting point. The generated Makefile includes oauth-credentials, oauth-preflight, oauth-evidence, oauth-dev-browser-smoke, oauth-browser-smoke, oauth-callback, browser-smoke, and passkey-browser-smoke targets for local OAuth UI regression checks, live provider readiness, redacted event evidence, callback evidence, page checks, and WebAuthn checks. Generated manifests depend on the single wasi-auth crate plus ddd_cqrs_es; they never contain cross-repository path dependencies. This source checkout uses .cargo/config.toml patches only for local alpha verification, while generated projects retain publishable registry manifests.

Generated Manifest

Every generated project includes ddd.toml. The CLI uses this file to know what it can safely patch later.

Example shape:

[project]
name = "billing"
preset = "basic"
runtime = "spin"
db = "sqlite"
realtime = "off"
transport = "http"
ui = "none"

[capabilities]
enabled = []

[domains.invoice]
aggregate = "Invoice"
module = "invoice"
commands = ["CreateInvoice"]
events = ["InvoiceCreated"]

Fullstack projects also include stable auth and authorization configuration sections. Provider entries reference environment variable names only; the CLI never writes OAuth client secrets into source files.

Passkeys are disabled by default until AUTH_ENABLE_PASSKEYS=true. Configure AUTH_PASSKEY_RP_ID, AUTH_PASSKEY_RP_NAME, AUTH_PASSKEY_ORIGIN, and AUTH_PASSKEY_CHALLENGE_TTL_SECONDS in .env or Spin variables before testing WebAuthn in a browser. For local browser passkeys, use localhost as the RP ID and visit http://localhost:3008, not the numeric loopback URL.

[auth]
issuer = "http://127.0.0.1:3008"
audience = "fullstack-app"
access_token_ttl_seconds = 900
refresh_token_ttl_seconds = 2592000
cookie_mode = "http-only"

[[auth.providers]]
provider_id = "google"
issuer = "https://accounts.google.com"
scopes = ["openid", "email", "profile"]
enabled_env = "AUTH_GOOGLE_ENABLED"
client_id_env = "AUTH_GOOGLE_CLIENT_ID"
client_secret_env = "AUTH_GOOGLE_CLIENT_SECRET"
userinfo_url_env = "AUTH_GOOGLE_USERINFO_URL"

[[auth.providers]]
provider_id = "facebook"
issuer = "https://www.facebook.com"
scopes = ["email", "public_profile"]
enabled_env = "AUTH_FACEBOOK_ENABLED"
client_id_env = "AUTH_FACEBOOK_CLIENT_ID"
client_secret_env = "AUTH_FACEBOOK_CLIENT_SECRET"
userinfo_url_env = "AUTH_FACEBOOK_USERINFO_URL"

[[auth.providers]]
provider_id = "apple"
issuer = "https://appleid.apple.com"
scopes = ["openid", "email", "name"]
enabled_env = "AUTH_APPLE_ENABLED"
client_id_env = "AUTH_APPLE_CLIENT_ID"
generated_client_secret_env = "AUTH_APPLE_GENERATED_CLIENT_SECRET"
team_id_env = "AUTH_APPLE_TEAM_ID"
key_id_env = "AUTH_APPLE_KEY_ID"
private_key_env = "AUTH_APPLE_PRIVATE_KEY"
client_secret_ttl_seconds_env = "AUTH_APPLE_CLIENT_SECRET_TTL_SECONDS"

[authorization]
provider = "embedded-cedar"
policy_revision = "embedded-v1"
default_decision = "deny"

AUTH_PRODUCTION_MODE=false keeps the generated stack simple for local development. Set AUTH_PRODUCTION_MODE=true only with a pre-provisioned AUTH_JWT_KEY_RING_JSON containing ES256 keys, HTTPS secure cookies, CSRF secrets, and a production mail adapter. Administration is derived exclusively from an MFA-authenticated VerifiedAuthContext; there is no admin-token field or header. Production mode rejects runtime HS256 defaults, capture mail, and missing secrets.

Generated fullstack projects also expose AUTH_COOKIE_SECURE. Keep it false for local HTTP development and set it to true for HTTPS deployments so browser sessions are issued with Secure, HttpOnly, and SameSite=Lax. The generated Makefile and Spin manifests pass the declared auth variables into the runtime component; changing JWT, OAuth, passkey, cookie, or public-base-url values in .env or Spin variables is intended to affect the running auth stack.

If a project does not have ddd.toml, treat it as outside the supported generated-project patching path unless you intentionally adopt it.

Add Domain Code

Run ddd add ... from the generated project root, or pass --cwd <project>.

Add a second aggregate:

ddd add aggregate BillingAccount

Add an event to an existing aggregate:

ddd add event Invoice InvoicePaid --field amount:i64 --field paid_at:String --event-type invoice_paid

Add a command:

ddd add command Invoice PayInvoice --field amount:i64

Field syntax is name:RustType. The CLI inserts generated variants into marker regions in the generated domain module and updates ddd.toml.

Available add targets:

aggregate
event
command
error
projection
query
process-manager
snapshot
upcaster
route
grpc-method
server-fn
rest-endpoint
test

Common examples:

ddd add projection InvoiceLedger
ddd add query InvoiceSummary
ddd add process-manager PaymentSaga
ddd add snapshot InvoiceSnapshot
ddd add upcaster InvoicePaid --from 1 --to 2
ddd add route invoice-summary --method GET --path /api/invoices/summary
ddd add rest-endpoint invoice-payments --method POST --path /api/invoices/payments
ddd add server-fn pay-invoice
ddd add grpc-method pay-invoice
ddd add test invoice-payment

Enable Capabilities

Use ddd enable ... when the project exists and you want to wire a capability into ddd.toml and, where applicable, Cargo.toml feature flags.

ddd enable db postgres
ddd enable db mysql
ddd enable redis-store
ddd enable realtime redis
ddd enable grpc
ddd enable rest
ddd enable leptos
ddd enable auth
ddd enable authz
ddd enable passkeys
ddd enable oauth-provider google
ddd enable oauth-provider apple
ddd enable oauth-provider facebook
ddd enable idempotency
ddd enable snapshots
ddd enable tracing

Use dry-run JSON before enabling a capability in automation:

ddd --dry-run --format json enable realtime redis

Runtime Matrix

The CLI currently scaffolds Spin-focused apps.

Supported values:

AxisValues
Runtimespin
DBsqlite, postgres, neon, supabase, turso, mysql, redis
Realtimeoff, polling, redis
Transporthttp, grpc, both
UInone, leptos

Use the CLI to inspect the live matrix:

ddd matrix
ddd capabilities --json

Redis has two separate meanings:

  • db=redis means Redis is the durable event/checkpoint/read-model store.
  • realtime=redis means Redis is only the wake/notification transport unless db=redis is also selected.

Spin supports transport=http, transport=grpc, and transport=both.

Serve, Watch, and Fresh

The runtime commands read from ddd.toml and can be overridden with flags.

Preview the command:

ddd --dry-run --format json serve

Serve the app:

ddd serve
ddd serve --db postgres --realtime redis --transport http

Watch and restart:

ddd watch

Reset data only:

ddd fresh --db sqlite

fresh is reset-only. It should not start the server.

Agent and MCP Workflow

Agents should use JSON dry-runs before changing files:

ddd --cwd billing --dry-run --format json add event Invoice InvoicePaid --field amount:i64

The report includes:

{
  "status": "planned",
  "message": "project extension complete",
  "operations": [
    {
      "action": "update",
      "path": "src/domain/invoice.rs",
      "bytes": 2048,
      "description": "add domain event"
    }
  ]
}

A safe agent loop is:

  1. Run ddd capabilities --json.
  2. Run ddd matrix if runtime/backend choices matter.
  3. Run the mutating command with --dry-run --format json.
  4. Inspect operations.
  5. Apply the same command without --dry-run.
  6. Run ddd check.
  7. Run project tests.

File and Symbol Targeting

The current CLI targets generated projects through ddd.toml, aggregate names, and marker regions.

This syntax is not currently implemented:

/path/to/file.rs:Struct
/path/to/file.rs:EnumVariant
/path/to/file.rs:function_name

If you need path or symbol targeting, add explicit CLI support and tests first, or patch the file manually with Rust-aware edits. Do not pass unsupported selector syntax to ddd.

Release Pairing

The library and CLI are versioned together.

For maintainers:

make version 0.2.5
make publish dry-run

Reliable publish dry-run forms are:

make publish dry-run
make publish -- --dry-run

Real publish:

make publish

Run cargo login first, or provide CARGO_REGISTRY_TOKEN in the environment.

The release script validates that ddd_cqrs_es and ddd-cqrs-es-cli have matching versions, then publishes ddd_cqrs_es before ddd-cqrs-es-cli.

Troubleshooting

ddd check fails with missing generated files:

  • Run it from the generated project root or pass --cwd <project>.
  • Confirm ddd.toml, Cargo.toml, and src/domain/mod.rs exist.

ddd add event cannot find an aggregate:

  • Use the aggregate name from ddd.toml.
  • Run ddd add aggregate <Name> first if the aggregate does not exist.

--runtime wasmtime is rejected:

  • The CLI-generated runtime is currently Spin-only.
  • Use spin, or check ddd capabilities --json after upgrading the CLI.

A file already exists:

  • Inspect the file before using --force.
  • Prefer dry-run JSON to see exactly which path is colliding.