Skip to main content

Providers & rails

What this covers / who it's for. Which capabilities Stables executes itself and which it offloads to the underlying finance rail; what the platform owns vs what the provider owns; how provider truth reaches our records (webhooks + reconciliation); and why our view of a status can briefly lag the provider's. Read this to reason about latency, trust boundaries, and "who do we call when X is wrong".

One provider today: Conduit

Stables integrates exactly one external finance rail: Conduit. The provider registry is built to take a second provider as an additive entry (the architecture is replaceable, not concurrent multi-provider, in V1 — ADR D60), but today every regulated capability below runs on Conduit (rails module README; registry: app-modules/rails/config/rails.php).

Two ship-state facts a product person should know:

  • The base URL defaults to Conduit's sandbox. A deployment must explicitly point at production (packages/stables/conduit-sdk/config/conduit.php) — the default is sandbox precisely so a misconfigured environment can never accidentally touch production money. Sandbox and production are fully isolated (separate keys, separate data).
  • Outbound calls time out after 30 seconds by default (same config) — the ceiling on how long a synchronous provider-backed action can hang before failing.

What is offloaded (the capability surface)

The rails module defines 13 capability contracts — the complete, enumerable list of what the provider does for us (app-modules/rails/src/Contracts/):

CapabilityWhat the provider executes
Customer onboardingCreating/updating the business customer record on the rail (KYB)
Identity verificationKYC checks on persons, via the provider's KYC vendor
Customer managementReading/maintaining the provider-side customer
WalletsCustodial wallet creation and lifecycle
Wallet signersRegistering signing parties on a wallet
Signing quorumCo-sign / quorum rules for wallet actions
Virtual accountsFiat virtual account issuance
OrdersFX / conversion order execution
PayoutsOutbound payment execution
TransactionsThe provider-side transaction ledger (deposits included)
Whitelist recipientsApproved-recipient (whitelist) management
Registered addressesRegistered crypto address management
DocumentsReceiving the KYB/KYC documents we forward

The ownership split (handoff model, V1)

The platform ownsThe provider owns
The durable record: a local mirror of every provider entity, plus everything the provider never sees (teams, roles, preferences, cases, notifications)Custody of funds and wallets
The audit trail — every action, denial, and failure, tamper-evident (Compliance & trust)Actual money movement (payouts, orders, deposits settle on the rail)
Authorization and dual control — RBAC, maker/checker, co-sign approval flows run here before anything reaches the railThe KYC vendor relationship and verification decisioning
The encrypted document vault (we store first, then forward — Where data lives)SAR/CTR filing with regulators in the V1 handoff model — we prepare and record filings, the provider files (Compliance & trust)

Every outbound call is logged

Every request to the provider is recorded per attempt in the append-only central vendor request log: bodies are encrypted at rest, known-sensitive fields (credentials, tax IDs, account numbers, …) are redacted before persisting, and oversized bodies are truncated (16 KB cap). The log is transport telemetry, not the compliance record — it is pruned on a 90-day window (the authoritative KYB/KYC record lives in the document vault + onboarding mirror). Source: app-modules/rails/config/rails.php, rails README.

How provider truth comes back (and why our view can lag)

Provider-backed actions are submit-then-listen: we submit, the provider answers "accepted", and the real outcome (KYB progression, wallet created, payout settled, order filled) arrives later as a webhook (Async by design).

Inbound webhook mechanics (webhooks README):

  • Signed, with a replay window. Deliveries are HMAC-signature-verified; a delivery whose signed timestamp is more than 300 seconds old (or in the future) is rejected (app-modules/webhooks/config/webhook-client.php).
  • Exactly-once. Duplicate deliveries (same source + event id) are detected and skipped, so a provider retry can never double-apply a status change.
  • Processed on a dedicated queue (webhooks), separate from all other background work.

Consequence: our view is eventually consistent with the provider's. Normally the lag is seconds (webhook delivery + queue processing). If the webhooks queue is backed up or its worker is down, statuses freeze at their last-known value while the provider moves on — see Dependencies & failure modes.

The healing layers (when a webhook never arrives)

  1. The provider retries. Conduit redelivers failed webhooks on a retry ladder (30s → 2m → 15m → 1h → 4h), covering transient drops (docs/tracking/conduit-funding-deferrals.md).
  2. The hourly reconciliation sweep. Every hour, mirrors that have gone stale (no update within their expected window and not in a terminal state) are re-read directly from the provider API and healed (provider-mirror README, ADR D59). Limit: the sweep heals coarse status only. Some webhook-only sub-states (e.g. a deposit held for sender information) are not exposed by the provider's read API, so a permanently lost webhook for those specific transitions is not sweep-recoverable — the retry ladder is the cover for them (funding README).
  3. Drift records. When reconciliation finds the provider and our mirror disagree, the divergence is recorded as a drift record for operator review — field names and coarse change markers only, never raw PII values. Unresolved drift is never auto-deleted; it is live evidence until an operator resolves it (provider-mirror README).

What this means in practice

  • A status shown in Stables is the last thing the provider told us — trust it to seconds under normal operation, to about an hour under webhook loss (coarse states), with the named sub-state exceptions above.
  • "The provider says X but we show Y" is expected transiently; if it persists past the hourly sweep, look for a drift record — that is the designed operator surface for divergence.
  • Anything involving custody, settlement, or KYC decisioning is a provider conversation; anything involving who was allowed to do what, when, and what we recorded is ours.

← Product wiki index