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
VaultTokenRefcan 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:
- Vault-agnostic token refs
{ vaultId, tokenRef }— run two vaults during a migration. - Network tokens from the start.
- Exportable vault only — reject any vault that cannot contractually export tokens. (This is a hard vendor-selection requirement, not a nice-to-have.)
- Draw the CDE boundary now, even while the vault is rented.
- 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
| Implementation | vaultId | Network token | Status |
|---|---|---|---|
BasisTheoryVault | basis-theory | Configured at BT app level under Loop TRID (LOO-2207) | Built, gated on a confirmed tenant |
StubVault | stub | Modeled as always-provisioned (stub-trid) | Built — offline proofs |
LoopVault | loop (future) | Own CDE | Planned (SAQ D destination) |
See also
- The two seams —
TokenVault+TokenChargeProvider - Card migration — getting existing cards into the BT vault with zero re-entry
- PCI posture — why a rented exportable vault = SAQ A
- Routing & failover — charging the portable token
Source
services/payments/src/orchestration/token-vault.ts·basis-theory-vault.ts·stub-vault.tsservices/payments/tests/unit/orchestration/charge-cascade.spike.test.ts·scripts/spike-basis-theory.ts- ADR-0093 (LOO-2207 network tokens / TRID)