Skip to main content
Version: 0.15 (unstable)

Guardian Service Architecture

This document describes the logical decomposition of the Guardian system — the modules inside the server process and the clients/SDKs that talk to it. It complements docs/architecture/infra.md, which covers how that same process is packaged and deployed on AWS.

TL;DR

Guardian is one Rust binary (crates/server) that fronts two transports (HTTP + gRPC) over a shared service layer, persists Miden account state and deltas in Postgres, signs its responses with ACK keys, and is consumed by Rust and TypeScript clients plus the multisig SDKs that build on top.

Layered view

Sources for the boxes: server module declarations in crates/server/src/lib.rs:3-30, API submodules in crates/server/src/api/mod.rs, service handlers in crates/server/src/services/mod.rs.

Components

Transports — api/

Two transports, one set of service handlers. Default ports are :50051 (gRPC) and :3000 (HTTP/dashboard); both are configurable via the server builder.

  • gRPC (api/grpc.rs) is the primary surface for the Rust client. Schema lives in crates/server/proto/guardian.proto. Default bind: :50051.
  • HTTP (api/http.rs) hosts the REST routes for the TypeScript clients and the operator dashboard. Default bind: :3000. axum based. The TS Guardian client (@openzeppelin/guardian-client) is HTTP-only — it does not speak gRPC. This Rust-vs-TS transport split is the most common integration pitfall in the codebase.
  • Dashboard (api/dashboard.rs, api/dashboard_feeds.rs) carries operator endpoints: accounts list, account detail, proposals, deltas, snapshot, info. Uses session auth distinct from Guardian's per-account auth.
  • EVM (api/evm.rs) is the feature-gated EVM proposal API exposed when the server is built with the evm feature.

Both transports share the middleware stack (crates/server/src/middleware): request authentication, rate limiting, CORS, request IDs, audit-log emission.

Service handlers — services/

Per-RPC handlers. Each file owns one verb of the Guardian contract.

Service filePurpose
push_delta.rsApply a signed delta to an account's state.
get_delta.rs, get_delta_since.rsRead deltas by id / by cursor.
delta_commit.rsFinal commit step turning a proposal into a delta.
push_delta_proposal.rs, get_delta_proposal*.rs, sign_delta_proposal.rsMultisig proposal lifecycle.
get_state.rs, lookup_account.rs, configure_account.rsState and account lifecycle.
dashboard_account_*.rs, dashboard_global_*.rs, dashboard_info.rs, dashboard_pagination.rsOperator dashboard queries.

The transport layer (gRPC or HTTP) decodes the request, the middleware authenticates and audits it, then dispatches into one of these handlers.

Domain types

  • state_object.rs + delta_object.rs (crates/server/src/state_object.rs, crates/server/src/delta_object.rs) — canonical in-memory shapes for an account's state and the deltas applied to it. The proto-level shapes are normalized into these before any persistence touches them.
  • network/ — wraps the Miden RPC client used to verify on-chain proofs / submit transactions.
  • evm/ (crates/server/src/evm) — EVM proposal lifecycle, session auth, on-chain contract interaction. Feature-gated.
  • jobs/ (crates/server/src/jobs) — background work, currently canonicalization tasks that normalize proposals before commit.
  • audit/ (crates/server/src/audit) — append-only structured event log written alongside mutating operations.

Persistence — storage/ and metadata/

Two parallel module trees, each with a postgres.rs and filesystem.rs backend selected at startup:

In production both back ends are Postgres on RDS, sharing pool sizing controlled by GUARDIAN_DB_POOL_MAX_SIZE and GUARDIAN_METADATA_DB_POOL_MAX_SIZE (see infra.md). The filesystem backends are kept for local development and tests.

Storage modes

The backend is selected at compile time via Cargo features in crates/server/src/builder/storage.rs — not at runtime, not via env vars. Pick the right feature set for your build target.

ModeFeatureStorageMetadataAuditRequired env
Postgres (prod, staging)postgresPostgresServicePostgresMetadataStorePostgresAuditor — durable rows in admin_actionsDATABASE_URL
Filesystem (dev, tests)noneFilesystemServiceFilesystemMetadataStoreLogAuditor — structured logs only, no durable auditGUARDIAN_STORAGE_PATH, GUARDIAN_METADATA_PATH

Production must use Postgres

Filesystem mode is unsafe for production for three reasons:

  1. No durable audit trail. The filesystem build wires up LogAuditor and emits a one-shot startup warning that audit events flow only to structured logs (builder/storage.rs:129-137). Postgres mode persists them in the admin_actions table.
  2. No migrations, no schema guarantees. The Postgres path runs postgres::run_migrations at startup (builder/storage.rs:109); the filesystem path has none.
  3. No horizontal scaling. Multiple ECS tasks behind the ALB cannot share filesystem state — each task would diverge. Postgres on RDS is the only shared substrate in the deployment topology (infra.md).

The AWS deploy path therefore builds with GUARDIAN_SERVER_FEATURES=postgres (plus evm when needed). See SERVER_AWS_DEPLOY.md.

When filesystem mode is fine

  • Single-process examples under examples/.
  • Unit and integration tests that need a real backend but not a real DB.
  • Quick local iteration on non-storage code paths before spinning up Postgres.

For local development that exercises Postgres behavior, run a local Postgres (e.g. docker run -e POSTGRES_PASSWORD=… postgres) and build with --features postgres rather than relying on the filesystem backend.

Identity — ack/

Guardian signs its responses so that clients (and the multisig SDKs in particular) can verify the server is the same Guardian they trust.

  • ack/mod.rs wires the signer.
  • ack/miden_falcon_rpo/ and ack/miden_ecdsa/ hold the two scheme implementations.
  • ack/secrets_manager.rs pulls secret payloads into the filesystem keystore at startup. In dev keys are auto-generated; in prod they are loaded from the Secrets Manager IDs in GUARDIAN_ACK_FALCON_SECRET_ID / GUARDIAN_ACK_ECDSA_SECRET_ID, falling back to guardian-prod/server/ack-{falcon,ecdsa}-secret-key when unset (secrets_manager.rs:10-13). Terraform sets these env vars per stack so multi-stack deployments use scoped IDs.

Dashboard subsystem

The dashboard layer is a small system in its own right inside crates/server/src/dashboard: its own session/auth (authz.rs, middleware.rs), allowlist of operator public keys (allowlist.rs), permission model (permissions.rs), shared state (state.rs), and pagination cursor logic (cursor.rs).

It piggybacks on the same Postgres backend as the rest of the server but authenticates operators through Falcon-signed challenges rather than the per-account credentials used by Guardian's primary API.

Consumers

Smoke harnesses under examples/ drive each SDK end-to-end; see the matching smoke-test-* skills for how to run them.

Authentication shape

Two distinct auth domains:

  1. Per-account auth — every mutating Guardian RPC is signed by the account's Falcon or ECDSA key. Verified in the auth middleware via metadata/auth/ which loads credentials from the metadata store and dispatches to the right scheme (miden_falcon_rpo.rs, miden_ecdsa.rs).
  2. Operator auth — dashboard endpoints use Falcon-signed challenges against an allowlist of operator public keys, producing session cookies. Lives entirely in dashboard/authz.rs.

Both paths share the same ACK signer when emitting responses; only the incoming credentials differ.

Where to look next