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
Fullstack SaaS product (recommended starter)
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
| Command | Fullstack |
|---|---|
ddd check | Yes — validates product file inventory |
ddd serve / ddd watch | Yes — make dev (+ cargo-watch for watch) |
ddd fresh | Yes — make db=postgres fresh (Postgres must be up) |
ddd enable auth / oauth-provider / passkeys | Manifest bookkeeping only; still set Spin/.env secrets |
ddd add aggregate | Yes — src/domain + src/domain_app (InMemory demo store) + /api/domain/... REST + lib/rest hooks |
ddd add event / ddd add command | Yes — 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:
| Option | Purpose |
|---|---|
--cwd <path> | Run against a generated project without changing the shell directory. |
--dry-run | Preview file operations or runtime command resolution without writing or executing. |
--format text|json | Choose human text or machine-readable JSON output. |
--force | Allow overwriting files that would otherwise be protected. Inspect collisions first. |
--yes | Reserved for non-interactive confirmations as workflows grow. |
The main command groups are:
| Command | Use |
|---|---|
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 serve | Resolve and run the app through the selected Spin runtime command. |
ddd watch | Resolve a rebuild/restart loop for the selected runtime command. |
ddd fresh | Reset schema/data only. This does not start the server. |
ddd doctor | Inspect required local tools. |
ddd check | Validate the generated project manifest and required files. |
ddd matrix | Print supported backend/realtime/transport combinations. |
ddd capabilities --json | Expose the machine-readable CLI contract for agents and MCP tools. |
Presets
Choose a preset with ddd init --preset <preset>.
| Preset | Best For | Default Shape |
|---|---|---|
basic | Learning the framework or building a pure domain crate. | Aggregate, command, event, fixture test, and in-memory example. |
leptos-wasi | Full-stack app scaffolding for Leptos WASI on Spin. | Domain/application/store/server boundaries with REST/SSE and optional gRPC. |
fullstack | Production-oriented Spin fullstack application. | Leptos islands, complete account/organization/admin pages, trusted auth, embedded Cedar, REST, and unary plus streaming gRPC. |
native-api | Native Rust API service shape. | Axum-style API scaffold with native SQL adapter features. |
worker | Projection or process-manager workers. | Worker entrypoint plus projection/process-manager-oriented stubs. |
custom | A 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:
| Axis | Values |
|---|---|
| Runtime | spin |
| DB | sqlite, postgres, neon, supabase, turso, mysql, redis |
| Realtime | off, polling, redis |
| Transport | http, grpc, both |
| UI | none, leptos |
Use the CLI to inspect the live matrix:
ddd matrix
ddd capabilities --json
Redis has two separate meanings:
db=redismeans Redis is the durable event/checkpoint/read-model store.realtime=redismeans Redis is only the wake/notification transport unlessdb=redisis 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:
- Run
ddd capabilities --json. - Run
ddd matrixif runtime/backend choices matter. - Run the mutating command with
--dry-run --format json. - Inspect
operations. - Apply the same command without
--dry-run. - Run
ddd check. - 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, andsrc/domain/mod.rsexist.
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 checkddd capabilities --jsonafter upgrading the CLI.
A file already exists:
- Inspect the file before using
--force. - Prefer dry-run JSON to see exactly which path is colliding.