How to run Guardian on a developer machine, what choices you have, and which example to reach for once it's up.
For protocol concepts (State, Delta, Nonce, Commitment) read
spec/index.md first — this guide assumes you know them.
For the deployed AWS topology see
docs/architecture/infra.md.
Three decisions when running Guardian locally:
- Storage backend — filesystem (default) or Postgres. See Storage modes. Filesystem is fine for most local work; pick Postgres only if you are testing migrations, audit persistence, or multi-replica behavior.
- Cargo features —
postgresand/orevm. Default builds do not include EVM routes and do not include the Postgres backend. - How to launch —
cargo run(fastest iteration) ordocker compose(closer to the deployed shape).
- Rust toolchain pinned by
rust-toolchain.toml. - Node 18+ if you will run any TS examples or packages.
- Docker if you will use
docker-compose.*.yml. - A Miden node — required for almost every flow. Either point at a
Miden Devnet endpoint or run one locally; configure via
GUARDIAN_NETWORK_TYPE. A node on a non-default host or port (for example a sidecar container) is reachable viaGUARDIAN_MIDEN_RPC_ENDPOINTwithout changing the network type; see CONFIGURATION.md for that and the optionalGUARDIAN_MIDEN_RPC_TIMEOUT_MS/GUARDIAN_MIDEN_RPC_MAX_ATTEMPTSread-retry knobs. The node's Miden line must match the workspace baseline (currently the 0.16 pre-release line; devnet already runs the 0.16 node) — a mismatched node is rejected at the RPC boundary (see Troubleshooting).
The server calls dotenvy::dotenv() on startup, so cargo run --bin server
automatically reads a root .env file when one exists. In practice a .env
file (or exported variables) is required: GUARDIAN_NETWORK_TYPE has no
default and the server refuses to start without it, and the built-in
filesystem paths live under /var/guardian, which may not exist or be
writable on a developer machine.
Minimal local .env:
GUARDIAN_STORAGE_PATH=.guardian/storage
GUARDIAN_METADATA_PATH=.guardian/metadata
GUARDIAN_KEYSTORE_PATH=.guardian/keystore
GUARDIAN_NETWORK_TYPE=MidenDevnet
RUST_LOG=infoCreate the directories once before the first run:
mkdir -p .guardian/storage .guardian/metadata .guardian/keystoreUse .env.example as a broader template when you need deploy variables,
Postgres, dashboard operators, or EVM settings. Docker Compose does not inject
the root .env into the server container by default; the checked-in compose
file already sets the container filesystem paths.
cargo run --bin serverThis builds with no extra features and uses the filesystem backend. Useful env:
| Variable | Notes |
|---|---|
GUARDIAN_STORAGE_PATH |
Local path for state + deltas. Defaults to /var/guardian/storage. |
GUARDIAN_METADATA_PATH |
Local path for accounts, auth, network. Defaults to /var/guardian/metadata. |
GUARDIAN_KEYSTORE_PATH |
ACK key files, auto-generated on first run. Defaults to /var/guardian/keystore. |
RUST_LOG (info) |
info, debug, or e.g. server::jobs::canonicalization=debug. |
GUARDIAN_NETWORK_TYPE (required) |
Miden network name: MidenLocal, MidenTestnet, or MidenDevnet. The server refuses to start when it is unset or unrecognized. |
At startup the server emits a warning that audit events will not be
persisted — that's expected for filesystem mode
(builder/storage.rs:133).
The HTTP server binds on :3000, gRPC on :50051.
docker compose -f docker-compose.postgres.yml up -d
DATABASE_URL=postgres://guardian:guardian@localhost:5432/guardian \
cargo run -p guardian-server --features postgres --bin serverThe Postgres path runs SQL migrations on startup
(builder/storage.rs) and
wires PostgresAuditor so admin actions land in the admin_actions table.
Pool sizing is controlled by GUARDIAN_DB_POOL_MAX_SIZE and
GUARDIAN_METADATA_DB_POOL_MAX_SIZE.
The guardian-server tests that need a live database (metadata replay state,
delta storage fencing, the audit append-only trigger, and the lease, session,
and challenge coordination stores) stay #[ignore] and run through one script:
POSTGRES_PASSWORD=guardian docker compose -f docker-compose.postgres.yml up -d postgres
./scripts/test-postgres.shName the postgres service explicitly: an unqualified up -d also builds and
starts the server image, which these tests do not use. POSTGRES_PASSWORD is
required even when only the database service is selected, because Compose
interpolates the whole file; the script's default connection URL expects
guardian. If your Compose volume was initialised with a different password,
pass a matching DATABASE_URL instead of relying on the default.
The suite drops and recreates the public schema before the first test and
re-applies every migration from empty, so no run inherits state from an earlier
one and there is no cleanup step. It creates guardian_test if it is missing,
and it refuses to run against a database whose name does not end in _test, so
a DATABASE_URL still exported from a Path B session cannot lose your
development data. Point it elsewhere with:
DATABASE_URL=postgres://user:pass@host:5432/guardian_test ./scripts/test-postgres.shRun one suite at a time per server: the reset wipes the shared test database. Execution is serial because one test reverts the newest migration for the duration of its own run.
The local compose Postgres uses no TLS, so omit sslmode (plaintext). To
exercise certificate verification locally, run a TLS-enabled Postgres with a
self-signed CA and point the server at it:
# Generate a CA and a server cert whose SAN matches the connection host
openssl req -x509 -newkey rsa:2048 -nodes -keyout ca.key -out ca.pem \
-subj "/CN=Test CA" -days 3650
openssl req -newkey rsa:2048 -nodes -keyout server.key -out server.csr \
-subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial \
-out server.crt -days 3650 -copy_extensions copyall
# start postgres with ssl=on using server.crt/server.key, then:
# verify-full: chain + hostname (SAN must be localhost)
DATABASE_URL="postgres://guardian:guardian@localhost:5432/guardian?sslmode=verify-full&sslrootcert=$PWD/ca.pem" \
cargo run -p guardian-server --features postgres --bin server
# verify-ca: chain only (hostname not checked)
DATABASE_URL="postgres://guardian:guardian@localhost:5432/guardian?sslmode=verify-ca&sslrootcert=$PWD/ca.pem" \
cargo run -p guardian-server --features postgres --bin serverExpected: correct CA + matching SAN → starts; wrong/empty CA file → fails fast
with a certificate error; under verify-full a SAN ≠ localhost is refused
while the same cert is accepted under verify-ca. See the full matrix in
CONFIGURATION.md → Database TLS.
GUARDIAN_EVM_RPC_URLS=31337=http://127.0.0.1:8545 \
GUARDIAN_EVM_ENTRYPOINT_ADDRESS=0x... \
cargo run -p guardian-server --features evm --bin serverEVM routes (/evm/auth/*, /evm/accounts, /evm/proposals*) only register
when the evm feature is on. Combine with postgres for prod-like local
setups: --features postgres,evm. Pair with an Anvil node — the
smoke-test-evm-proposal-support skill walks through the full flow.
docker compose up --build -d
docker compose logs -fThis is the default Compose flow — filesystem backend, no Postgres, no
root .env required. For a Postgres-backed compose stack use
docker-compose.postgres.yml. Endpoints are the same as Path A (:3000,
:50051).
| Goal | Features | Backend |
|---|---|---|
| Hack on a service handler quickly | none | filesystem |
| Touch migrations, audit, or multi-replica behavior | postgres |
Postgres |
| Exercise the EVM proposal flow | evm |
filesystem |
| Reproduce a prod issue locally | postgres,evm |
Postgres |
| Run the operator dashboard | any | either (Postgres for durable history) |
The deploy script builds with postgres,evm when the EVM stack is requested
— see SERVER_AWS_DEPLOY.md.
curl http://localhost:3000/ # liveness (alias of /status)
curl http://localhost:3000/pubkey # ACK key commitment
grpcurl -plaintext \
-import-path crates/server/proto -proto guardian.proto \
-d '{}' localhost:50051 guardian.Guardian/GetPubkeyIf GetPubkey returns a key, the server is wired correctly and the gRPC
target group's health check
(infra/alb.tf:55) would pass in production.
| Example | What it exercises | SDK |
|---|---|---|
examples/demo |
End-to-end multisig flow in a Rust TUI — recommended starting point. | Rust multisig client |
examples/rust |
Low-level Rust binaries for both local-node and mockchain flows. | Rust client |
examples/smoke-web |
Browser harness for multisig + wallet integrations. | TS multisig client |
examples/operator-smoke-web |
Local Falcon operator login + dashboard account APIs. | @openzeppelin/guardian-operator-client |
examples/evm-smoke-web |
EVM proposal lifecycle against Anvil + an EVM-enabled server. | @openzeppelin/guardian-evm-client |
examples/web |
Reference web integration. | TS multisig client |
Follow each example's README to drive it manually. Agents in this repo
have matching skills (smoke-test-rust-multisig-sdk,
smoke-test-ts-multisig-sdk, smoke-test-operator-dashboard,
smoke-test-evm-proposal-support) that automate the same flows.
cargo test --workspace
cargo test -p guardian-server --features integration
cargo test -p guardian-server --features e2eTypeScript packages each carry their own npm test — see the root
README.md.
Cargo feature gates (integration, e2e) document what each suite
needs at the top of the relevant test modules under
crates/server/src/testing. Agents in
this repo have a guardian-validation-matrix skill that picks the
smallest meaningful set for a given change.
DATABASE_URLmissing under--features postgres— the builder fails fast with"DATABASE_URL environment variable is required"(builder/storage.rs:97).- Filesystem dirs don't exist — the server creates them on demand, but the parent path must be writable.
- ACK keypair changes between runs in filesystem mode — keys auto-generate on first start. If clients pinned an old pubkey, point them at the new one or persist the keystore directory.
- gRPC reflection over Cloudflare — works locally but Cloudflare's free tier rejects gRPC unless explicitly enabled on the zone.
- Apple Silicon +
--platform linux/amd64— slow but works. For faster local Docker builds, buildlinux/arm64images and deploy withcpu_architecture = "ARM64".