Skip to main content
Version: 0.15 (unstable)

Local Development

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.

What you're choosing

Three decisions when running Guardian locally:

  1. 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.
  2. Cargo featurespostgres and/or evm. Default builds do not include EVM routes and do not include the Postgres backend.
  3. How to launchcargo run (fastest iteration) or docker compose (closer to the deployed shape).

Prerequisites

  • 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.

Environment file

The server calls dotenvy::dotenv() on startup, so cargo run --bin server automatically reads a root .env file when one exists. A .env file is not strictly required for the default filesystem server, but it is recommended for local cargo run because the built-in filesystem defaults 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=info

Create the directories once before the first run:

mkdir -p .guardian/storage .guardian/metadata .guardian/keystore

Use .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.

Path A — cargo run with filesystem (fastest)

cargo run --bin server

This builds with no extra features and uses the filesystem backend. Useful env:

VariableNotes
GUARDIAN_STORAGE_PATHLocal path for state + deltas. Defaults to /var/guardian/storage.
GUARDIAN_METADATA_PATHLocal path for accounts, auth, network. Defaults to /var/guardian/metadata.
GUARDIAN_KEYSTORE_PATHACK 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 (MidenDevnet)Miden network name.

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.

Path B — cargo run with Postgres

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 server

The 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 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 server

Expected: 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.

Path C — cargo run with EVM support

GUARDIAN_EVM_RPC_URLS=31337=http://127.0.0.1:8545 \
GUARDIAN_EVM_ENTRYPOINT_ADDRESS=0x... \
cargo run -p guardian-server --features evm --bin server

EVM 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.

Path D — Docker Compose

docker compose up --build -d
docker compose logs -f

This 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).

Choosing a feature flag combo

GoalFeaturesBackend
Hack on a service handler quicklynonefilesystem
Touch migrations, audit, or multi-replica behaviorpostgresPostgres
Exercise the EVM proposal flowevmfilesystem
Reproduce a prod issue locallypostgres,evmPostgres
Run the operator dashboardanyeither (Postgres for durable history)

The deploy script builds with postgres,evm when the EVM stack is requested — see SERVER_AWS_DEPLOY.md.

Verifying the server is up

curl http://localhost:3000/                   # liveness
curl http://localhost:3000/pubkey # ACK key commitment
grpcurl -plaintext \
-import-path crates/server/proto -proto guardian.proto \
-d '{}' localhost:50051 guardian.Guardian/GetPubkey

If 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.

Reaching for an example

ExampleWhat it exercisesSDK
examples/demoEnd-to-end multisig flow in a Rust TUI — recommended starting point.Rust multisig client
examples/rustLow-level Rust binaries for both local-node and mockchain flows.Rust client
examples/smoke-webBrowser harness for multisig + wallet integrations.TS multisig client
examples/operator-smoke-webLocal Falcon operator login + dashboard account APIs.@openzeppelin/guardian-operator-client
examples/evm-smoke-webEVM proposal lifecycle against Anvil + an EVM-enabled server.@openzeppelin/guardian-evm-client
examples/webReference 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.

Running tests

cargo test --workspace
cargo test -p guardian-server --features integration
cargo test -p guardian-server --features e2e

TypeScript 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.

Common gotchas

  • DATABASE_URL missing 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, build linux/arm64 images and deploy with cpu_architecture = "ARM64".