PaymentsVault & tokens

Vault & tokens

What this is: how a card becomes a token Loop owns and can charge on any processor — the custody half of the system — and why owning the Token Requestor ID (TRID) is the difference between portable and stranded card-on-file.

Who it’s for: anyone implementing a vault adapter, reasoning about card portability, or evaluating a vault vendor.

What to read next: The two seams · Card migration · PCI posture.

Why a portable vault is the foundation

High-risk processing means processors and vaults can freeze or drop accounts with little notice. If card data lives inside a processor’s vault, deplatforming takes the card-on-file with it. So the first design rule is: a card is custody, not processing, and custody must be portable.

The whole chain charges the token, not the card. That is why the vault sits first in the architecture: everything downstream — routing, failover, subscriptions — operates on a VaultTokenRef.

Basis Theory — the rented exportable vault

The production target is Basis Theory (BasisTheoryVault), a rented vault that keeps Loop at SAQ A while handing Loop a token it can move. It matches the proven loopbio-v2 pipeline: a card is vaulted via POST /tokens, then charged later by detokenizing through POST /proxy into a PSP.

The adapter owns custody (vault + read); the charge step lives in a TokenChargeProvider that uses the BT proxy. The two are separate by design — see The two seams.

⚠️

Built, not wired. BasisTheoryVault is implemented but not instantiated anywhere by default. Today it is exercised only by a gated spike runner (scripts/spike-basis-theory.ts) against a Basis Theory TEST tenant; the unit/CI proofs use StubVault, so they need no key and make no network call. Wiring it into the service is a later ticket (LOO-2189 / LOO-2224) once a confirmed test/live tenant is provisioned. The API key is always supplied from a secret ref — never hardcoded, never logged.

The vault never logs a PAN

BasisTheoryVault.vaultCard returns only non-sensitive metadata — brand, last4, expiry, and a token ref — never the PAN or CVC. Reads (getCard) return the same non-sensitive VaultedCard. The raw-PAN entrypoint exists only for server-side tests, the spike, and the gated migration path; the hosted-checkout flow tokenizes client-side so a PAN never enters the service process.

Vault-agnostic token references

A token ref names which vault holds the card, so two vaults can run during a migration and any charge can target the right one (ADR-0093 rule 1):

export type VaultTokenRef = {
  vaultId: string;   // "basis-theory" | "stub" | (future) "loop"
  tokenRef: string;  // opaque token id within that vault
};

This is what makes a vault swap possible without a rewrite — the LoopVault (own CDE, SAQ D) is a later implementation of the same TokenVault interface, addressed by a different vaultId.

Network tokens — and owning the TRID

A network token is a card-network-issued surrogate for the PAN that survives card reissue/expiry (the issuer updates it behind the scenes). ADR-0093 mandates network tokens from day one. But there is a critical, easy-to-miss correction from the red-team:

🚫

Network-token portability is TRID-dependent. A network token provisioned under a merchant-owned Token Requestor ID (TRID) is portable across acquirers. A network token provisioned under a PSP-owned TRID must be re-provisioned if you leave that PSP — it does not port. Therefore the rule is absolute: Loop must own its TRID. Never let a PSP own the requestor.

The TokenVault types carry this intent explicitly:

From services/payments/src/orchestration/token-vault.ts:

/** Network-token provisioning state (ADR-0093: own-TRID portability). */
export type NetworkTokenInfo = {
  provisioned: boolean;
  /** The Token Requestor ID the network token is bound to (Loop-owned ⇒ portable). */
  tokenRequestorId?: string;
};

In BasisTheoryVault, network-token provisioning under a Loop-owned TRID is configured at the BT application level and surfaced here once enabled (LOO-2207). StubVault models a Loop-owned-TRID network token as always provisioned (tokenRequestorId: "stub-trid") so portability assertions in the spike have something to read.

The portability guarantee

Two independent axes of portability come together:

  • Cross-PSP (from the vault): the same VaultTokenRef can be charged on any processor via the routing layer. Proven offline in the spike — the same token is presented to two PSPs and captured exactly once.
  • Cross-acquirer (from the TRID): a network token under Loop’s own TRID survives moving between acquirers without re-provisioning.

Together they mean a card-on-file survives a provider swap, a card reissue, and — because token refs are vault-agnostic — eventually a vault swap.

The five rules that keep portability (and the SAQ-D option) open

From ADR-0093:

  1. Vault-agnostic token refs { vaultId, tokenRef } — run two vaults during a migration.
  2. Network tokens from the start.
  3. Exportable vault only — reject any vault that cannot contractually export tokens. (This is a hard vendor-selection requirement, not a nice-to-have.)
  4. Draw the CDE boundary now, even while the vault is rented.
  5. Treat deplatforming as routine — health monitoring, auto-failover, a re-vault runbook.

Plus the v2 invariant: own the TRID (rule 6).

”Own gateway” flows

Where an entity holds a direct acquiring / own-gateway relationship, ADR-0093 says to prefer a proxy-vault (VGS) that forwards detokenized data to arbitrary endpoints while staying SAQ A — before concluding raw-PAN / SAQ D is required. The proxy pattern (BT POST /proxy, or a VGS forward proxy) is how a rented vault charges a gateway that expects raw card data without putting Loop in scope.

Implementations at a glance

ImplementationvaultIdNetwork tokenStatus
BasisTheoryVaultbasis-theoryConfigured at BT app level under Loop TRID (LOO-2207)Built, gated on a confirmed tenant
StubVaultstubModeled as always-provisioned (stub-trid)Built — offline proofs
LoopVaultloop (future)Own CDEPlanned (SAQ D destination)

See also

Source

  • services/payments/src/orchestration/token-vault.ts · basis-theory-vault.ts · stub-vault.ts
  • services/payments/tests/unit/orchestration/charge-cascade.spike.test.ts · scripts/spike-basis-theory.ts
  • ADR-0093 (LOO-2207 network tokens / TRID)