Production-ready
The checklist the wallet and the ledger meet, and how they meet each item, with the page that builds it. Check your own gRPC services against it.
The wallet and the ledger meet each item. Check your own gRPC services against them:
| Item | How | Where |
|---|---|---|
| Every setting sourced and checked; a bad one exits 78, naming its variable | config.Struct and the packages’ checks, in both services | The wallet’s settings, The ledger’s settings |
| No secret in the environment, the logs or the spans | PostgreSQL’s password and the private keys are files, read through s.Host.FS. The token is metadata, which otelgrpc doesn’t record, and no handler logs it, which a test of the Apps checks | The secrets, as files, Their log records |
| Certificates and signing keys renewed without a restart | internal/certfile, for both servers’ certificates and the wallet’s client certificate. The key set is fetched again for a kid the wallet lacks | The certificates, read again when renewed, The fetched key set |
| Two replicas of each service | a PodDisruptionBudget of minAvailable: 1, and a rolling update of maxUnavailable: 0 | The Deployments, The disruption budgets |
| Ready once it has reached its dependency, and still ready through its outages | Connected, and the ledger’s latch on PostgreSQL | Readiness: Connected, The ledger’s probes, Readiness |
| A rollout loses no call | grpcserver.Main’s 5s drain. A unary call is bounded by LEDGER_TIMEOUT, 10s, inside the 15s grace period. Watches end with Unavailable and are opened again. terminationGracePeriodSeconds is 30, above the drain, the grace period and the telemetry’s shutdown | The drain, Relaying the watch |
| Calls spread across pods | MaxConnectionAge on both servers | MaxConnectionAge |
| Money is never made or lost | one transaction per transfer, locks in the order of the IDs, and a CHECK that no account but external goes below zero | schema.sql, A transfer’s transaction |
| A retry never moves money twice | every transfer’s ID: the subject and the request’s ID | Why every transfer is safe to retry |
| The contract never breaks a caller | buf breaking against main, in CI | buf’s rules, buf generate |
| Least privilege | the ledger’s role has SELECT, INSERT and UPDATE on its two tables. Only the internal CA’s certificates call the ledger. A token can use only its subject’s account. A non-root, read-only container, with no service account token | Running it on kind, mTLS, grpcauth, The subject’s account, The Deployments |
| Observable | traces across both services, logs and metrics over OTLP, deploy/alerts.yaml, deploy/dashboard.json, and alerts on each Deployment from the Collector’s Kubernetes cluster receiver | Observing |
| Failures say why | statuses with PreconditionFailure, BadRequest, ErrorInfo and RetryInfo details | The statuses, with details, The ledger’s answers, as the wallet’s, A key set that can’t be fetched |
| Failures are rehearsed | the failure injection cases, in CI and against staging through its own Target | The shared cases, The failure injection cases |
| Safe by default | FAULT_TRUST_SCOPE empty, so no scenario is trusted. Server reflection off. The probes ports serve gRPC health and nothing else | TrustScenario, The gRPC probes |