WalletD developers
Platform

Architecture overview

The four services behind the WalletD API, what is authoritative for money, how credentials are verified, and what one partner deployment contains.

This page is for the person deciding whether to build on WalletD, not just against it. It describes what runs, what holds the money, and which component you have to trust for which property.

Four services and an edge

The backend is four runtime components, not thirty: a gateway, an auth service, the walletd API, and ledgerd, the double-entry ledger.

The gateway terminates the edge. It proxies routes without rewriting bodies, allow-lists the headers each route may carry, and enforces two different things: how often a caller may arrive (rate budgets) and how many of its requests may be in flight at once (concurrency compartments). Both are on Environments, limits and quotas. The identity headers X-User-Id, X-Client-Id and X-Tenant-Id are not in any allowlist, so they cannot be spoofed through the edge.

authsvc is the only service that touches credentials. It issues tenant API keys, mints end-user tokens by exchanging a key, publishes the JWKS that verifies them, and audits every credential event. It stores only Argon2id hashes of key secrets.

walletd owns the money paths: the state machines, the parties, the policy, the limits, the fee schedule, the webhook outbox. It does not own the books. Every money module is written once against a single internal port that resolves a tenant to the one ledger that answers for it.

ledgerd is the ledger of record. Balanced multi-leg postings in integer minor units, two-phase reservations, preconditions, periods and statements, a sequenced event feed, and its own invariant verifier. It has its own database and its own database role, so walletd module code physically cannot reach money tables.

What is authoritative for money

Only ledgerd. This matters more than it sounds, because it is the answer to "what happens when two components disagree".

One ledger per tenant. A tenant is provisioned by creating its ledger first and then the row that names it, because the ledger is named by the tenant id and has to exist before anything points at it. tenants.ledger_id is NOT NULL: there is no tenant without a ledger and no configuration in which money lands somewhere else. Every ledgerd query is scoped by ledger id, and above that a consumer owns ledgers, so a ledger a caller does not own answers 404 identically to one that does not exist.

Two-phase postings. A money movement reserves first (a pending), the calling module commits its own state, then the reservation is posted or voided. ledgerd enforces that a post cannot exceed what its pending reserved, a rule that used to live in the payment layer and now lives where the money does. Every call is idempotent, so a retry is safe by construction.

Accounts open on first use. There is no provisioning step that can half-succeed: an owner key resolves to an account whenever a posting names one. The consequence worth knowing is the inverse one. The absence of an account proves nothing about the absence of a party.

The invariants are checked, not assumed. ledgerd verify runs every five minutes on a live deployment. Separately, the books have been recomputed from the raw tables with SQL rather than by asking the service whether it is happy, because a checker and the code it checks share their author's assumptions. Over 5.45 million entries that audit found zero exceptions on conservation, on cached-balance reconciliation, on floor enforcement with and without holds, and on cap claims, and the reporting projection reconciled entry for entry with the ledger of record across two databases. That corpus was load-generated, not production traffic: it is evidence about the mechanism, not about operating a live loop.

There is one place where a service boundary is visible to you. walletd commits its domain rows, its outbox events and the stored idempotent response in one database transaction, and the postings travel to ledgerd inside that window as keyed, replayable calls whose ids are derived from your Idempotency-Key. Nothing claims those two commits are one. The gap is closed by replay: a crash between the remote call and the local commit heals when the call is repeated, and a refusal asks the ledger of record for the stored result before it is allowed to stand. What this means for your code is that retrying with the same key is not merely permitted, it is how the system reaches a consistent answer. See Idempotency.

How a credential is verified

Nothing inside the cluster trusts the gateway for authorization. The header allowlists stop identity-header spoofing at the edge, but that is defence in depth, not the fence. walletd authenticates every /v1 request itself: API keys by introspecting them against authsvc, end-user tokens locally against the JWKS with issuer, expiry and algorithm pinned. A gateway-injected value would only ever be advisory.

Two kinds of principal come out of that. An API key acts tenant-wide, bounded by the scopes minted onto it. A user token carries scopes granted at exchange, bounded by the issuing key's scopes, and is pinned to its own user: even holding balances:read, a user token reading another user's balance is refused. Security for integrators has the credential formats, lifecycles and rotation.

ledgerd is not exposed at the public gateway at all. Its consumers authenticate with OIDC client credentials, and walletd is one of them.

What a partner deployment contains

Every partner runs its own stack. This is a commercial and a technical position at once: the subscription is per separately deployed partner, and no production PII, identity document, secret or ledger entry is copied into a central WalletD system by default.

A deployment carries the four services, the partner's own databases and keys, the identity realm, the channel applications, and the deployment's own operations. The partner owns the regulated business model and licences, customer terms and pricing, the bank and PSP contracts, KYC approval and case disposition, first-line customer service, and the data-controller decisions. WalletD supplies the service binaries and signed deployment artifacts, the infrastructure manifests and upgrade tooling, the specifications and SDKs, security fixes and incident assistance, and control evidence for its own software.

Delivery works the same way for every service: each repository runs its own gates and publishes an immutable, sortable image tag, one configuration repository declares what runs, and the cluster pulls from it. A service deploys when its own repository says so, and only that service moves.

Access to a partner's data by WalletD's own staff is not a matter of trust or policy prose. It is enforced in code, ticket-bound and time-limited, and described exactly on Support, SLAs and status.

What this architecture does not do

Stated plainly, because an evaluation finds these anyway and finding them in a table is better than finding them in a demo.

  • Cross-tenant money movement does not exist. Users, merchants, accounts and every movement live inside one tenant's loop.
  • Consumers cannot cash out to a bank. This is a semi-closed wallet. Money leaves through merchant payouts, which post to the ledger first and produce a statement that instructs the actual transfer.
  • Card data never touches the platform. The processor tokenizes client-side and walletd stores payment-intent ids only, which keeps the card scope at SAQ-A.
  • Sandbox and live are not separate data planes yet. The environment is carried on the credential and checked, but the split is a known gap rather than a shipped isolation boundary. See Environments, limits and quotas.
  • PostgreSQL row-level security is not the second fence yet. Tenant isolation is enforced in the repository layer on every query and again at posting time by ledger scope. RLS as a third, database-level fence is deliberately deferred and tracked.
  • Refunds are contra postings, not linked reversals. An auditor tracing a refund follows the refund transaction's reference back to the payment. The reversal linkage the schema provides is reserved and unexercised.

Going deeper

A security reviewer, a procurement team or an auditor asks for a different set of material next: the security pack, penetration test summaries, disaster recovery evidence, the compliance posture and the operational runbooks. None of that is on this site. It is maintained separately and is available on request through your WalletD contact or security@walletd.io.

Next

On this page