Skip to main content
Qustody is distributed as a single Go binary plus a PostgreSQL database. Most production deployments run it as a container behind a load balancer, with managed Postgres (AWS RDS, Aurora, or equivalent) and a separate signing service.

Prerequisites

  • Go 1.24+ if you build from source
  • PostgreSQL 16+ (the schema is auto-migrated on startup when CUSTODY_DB_MIGRATE_ON_START=true)
  • A reachable Quantum Chain RPC node (HTTP and optionally WebSocket)
  • An external post-quantum signing service (callback HTTP, remote HTTP, or gRPC)
  • TLS certificates for production (terminated at your load balancer or directly on the API)

Container image

A multi-stage Dockerfile is shipped at custody/Dockerfile:
# Build
docker build -t qustody-api:latest -f custody/Dockerfile .

# Run
docker run --rm -p 8080:8080 -p 9090:9090 \
  -e CUSTODY_DB_HOST=postgres \
  -e CUSTODY_DB_PASSWORD=secret \
  -e CUSTODY_NODE_RPC_URL=http://quantum-chain:8545 \
  -e CUSTODY_AUTH_ENABLED=true \
  qustody-api:latest
The container exposes:
  • 8080 — HTTP API
  • 9090 — Prometheus metrics (/metrics)

Local development with Docker Compose

A reference docker-compose.yaml lives at the repo root and brings up PostgreSQL, the Qustody API, and a development Quantum Chain node:
docker compose up -d postgres api
docker compose logs -f api
The development profile uses CUSTODY_AUTH_ENABLED=false and CUSTODY_RBAC_ENABLED=false to make exploration easy. Never deploy that combination.

Production deployment

Run at least two replicas behind a load balancer. The API is stateless; replicas coordinate through PostgreSQL row-level locks for transaction recovery.

Required environment variables

VariableDescription
CUSTODY_DB_HOST, CUSTODY_DB_PORT, CUSTODY_DB_NAME, CUSTODY_DB_USER, CUSTODY_DB_PASSWORDPostgreSQL connection
CUSTODY_DB_SSLMODESet to verify-full in production
CUSTODY_NODE_RPC_URLQuantum Chain HTTP RPC endpoint
CUSTODY_NODE_WS_URLQuantum Chain WebSocket endpoint (for deposit monitoring)
CUSTODY_AUTH_ENABLEDtrue
CUSTODY_RBAC_ENABLEDtrue
CUSTODY_WEBHOOK_SIGNING_KEYRandom 32-byte hex string
CUSTODY_WEBHOOK_ENCRYPTION_KEY64-hex-char (32 byte) AES-256-GCM key for webhook secret encryption at rest
CUSTODY_ENVproduction (enforces HTTPS for tenant and webhook URLs)
See the full list at Configuration reference.

Aurora / read-replica configuration

CUSTODY_DB_HOST=writer.cluster-xxx.us-east-1.rds.amazonaws.com
CUSTODY_DB_READ_REPLICA_ENABLED=true
CUSTODY_DB_READ_HOST=reader.cluster-ro-xxx.us-east-1.rds.amazonaws.com
CUSTODY_DB_IAM_AUTH=true
CUSTODY_DB_SSL_ROOT_CERT=/etc/ssl/rds-ca.pem
When CUSTODY_DB_IAM_AUTH=true, CUSTODY_DB_PASSWORD is not required — Qustody uses RDS IAM tokens.

Signer configuration (gRPC mode)

CUSTODY_SIGNER_TYPE=grpc
CUSTODY_SIGNER_BACKEND=enqlave    # or "qey"
CUSTODY_SIGNER_GRPC_TARGET=signer.internal:443
CUSTODY_SIGNER_GRPC_CLIENT_CERT=/etc/qustody/client.crt
CUSTODY_SIGNER_GRPC_CLIENT_KEY=/etc/qustody/client.key
CUSTODY_SIGNER_GRPC_CA_CERT=/etc/qustody/ca.crt
CUSTODY_SIGNER_GRPC_TIMEOUT=30s
For callback or remote modes see External signing.

SSO

CUSTODY_SSO_ENABLED=true
CUSTODY_SSO_JWT_SIGNING_KEY=<32 random bytes hex-encoded>
CUSTODY_SSO_ALLOWED_ISSUERS=https://login.example.com,https://accounts.google.com
CUSTODY_SSO_AUTO_PROVISION=true
CUSTODY_SSO_DEFAULT_ROLE=viewer

Compliance / AML

CUSTODY_COMPLIANCE_ENABLED=true
CUSTODY_COMPLIANCE_PROVIDER=shuftipro     # or "chainalysis", "noop"
CUSTODY_SHUFTIPRO_CLIENT_ID=...
CUSTODY_SHUFTIPRO_SECRET_KEY=...
CUSTODY_SHUFTIPRO_CALLBACK_URL=https://api.your-domain.com/v1/compliance/callback/shuftipro

Health and readiness

The API exposes two endpoints used by load balancers and orchestrators:
EndpointPurpose
GET /healthzLiveness — returns 200 OK if the process is alive
GET /readyzReadiness — returns 200 OK only when DB and signer are reachable
In Kubernetes:
livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 10
readinessProbe:
  httpGet: { path: /readyz, port: 8080 }
  periodSeconds: 5

Observability

SurfaceEndpoint / sink
Metrics:9090/metrics (Prometheus format)
TracingOpenTelemetry OTLP when CUSTODY_OTEL_ENABLED=true and CUSTODY_OTEL_ENDPOINT is set
LogsStructured JSON to stdout
HealthGET /healthz, GET /readyz
Enable rate limiting with Redis for distributed deployments:
CUSTODY_RATE_LIMIT_ENABLED=true
CUSTODY_RATE_LIMIT_RPM=120
CUSTODY_RATE_LIMIT_BURST=20
CUSTODY_RATE_LIMIT_REDIS_URL=redis://redis:6379/0

Database migrations

Migrations live in custody/store/postgres/migrations/. They are applied automatically on startup when CUSTODY_DB_MIGRATE_ON_START=true (default). For zero-downtime upgrades, run migrations from a single instance with CUSTODY_DB_MIGRATE_ON_START=true and start the rest with false.

Backups and disaster recovery

  • PostgreSQL is the source of truth. Use point-in-time recovery (PITR).
  • Audit log integrity is verifiable independently via GET /v1/audit/verify.
  • Webhook secrets are encrypted column-level when CUSTODY_WEBHOOK_ENCRYPTION_KEY is set; back up the key separately.