The Hyperswitch Pivot & PCI Boundary
The Hyperswitch Pivot & PCI Boundary
MVS Pay used to be a Finix-direct payment platform. It is now a thin set of MVS extensions wrapped around a self-hosted Hyperswitch orchestration stack. This page explains why that pivot happened, what was deleted, the design principle that governs every feature decision since (ADR-007), and how the PCI scope collapsed onto two Hyperswitch containers. If you are taking over the platform, read this before anything else — almost every other decision in the codebase is downstream of it.
The pivot is complete (status as of 2026-05-28). All milestones M0–M28 have
landed plus a gap-audit remediation pass. The narrative here records the original
rationale; where the built state differs from the original plan, this page calls it
out. Source of truth for the build status is docs/architecture/hyperswitch-pivot.md.
Why we pivoted
The Phase 4–7 work had already productionized MVS Pay as a Finix-direct platform. That was a large, working surface — and it was replaced wholesale. Concretely, the Finix-direct architecture was:
The replacement: rebuild on top of Hyperswitch, Juspay’s open-source payment
orchestration platform. Hyperswitch becomes the source of truth for the payment
domain — payment_intent, payment_attempt, refund, dispute, mandate,
customer, payment_method, merchant_account, connector configs, and routing
rules. Finix is demoted from “the platform” to “one connector among many,” plugged
in through Hyperswitch’s merchant_connector_account surface (see
The five connectors below).
The motivation
The Finix-direct path meant MVS owned, forever, the full payment-orchestration problem: connector adapters, routing logic, retry rules, dunning, settlement ledgers, dispute evidence handling, cost analytics. Hyperswitch already solves all of that as open-source under Apache-2.0 and is actively maintained. Rebuilding it MVS-side was a permanent maintenance tax competing against upstream — the exact trap ADR-007 was written to prevent.
Pivoting also unlocked features MVS would otherwise have had to build from scratch: Smart Retries, decision-engine multi-armed-bandit (MAB) routing, Cost Observability, Revenue Recovery, and OLAP analytics all ship as native Hyperswitch features we configure rather than implement.
Product reframing: MVS Pay is now a package
The pivot also changed what MVS Pay is. It is no longer just an app — it is a
package other MVS apps (healthos, mvs-c2, future apps) consume. Two npm packages
ship from the Buildkite Package Registry (mvs-cloud/pay/npm):
The SDK calls apps/mvs-pay-extensions — a Hono service (renamed from
payfac-gateway) that orchestrates Hyperswitch and persists only MVS-specific
extension data (Plaid-backed instant payouts, POS device management, residuals,
custom billing). The operator UI at apps/mvs-pay consumes the same SDK and
dogfoods @mvs/mvs-pay-ui for embedded flows.
For the surface-by-surface breakdown of these packages and apps, see Surfaces → Extensions Service, Surfaces → The SDK, and Surfaces → Checkout UI Components.
What was deleted
The pivot is a deletion story as much as an addition story. Packages removed:
packages/payments— the Finix provider / client. Gone entirely. There is no longer any direct Finix HTTP client in the repo; Finix is reached only through Hyperswitch’s connector adapter.packages/payfac-client— collapsed into@mvs/mvs-pay-sdk.apps/payfac-gateway— rewritten asapps/mvs-pay-extensions.packages/payfac-jobs(the 8 Finix sync workers) — replaced by thepay-cronsworker, which now ingests Hyperswitch webhooks instead of polling Finix.
And one rename: packages/payments-ui → packages/mvs-pay-ui.
These paths are gone from the working tree — apps/payfac-gateway,
packages/payments, and packages/payfac-client no longer exist. If you find a
reference to them, it is stale documentation or a comment, not live code. The
last commit where the Finix-direct architecture existed is tagged
pre-hyperswitch-pivot; git checkout pre-hyperswitch-pivot restores it if a
recovery is ever needed.
Schema: 43 tables → ~15
The Prisma schema in packages/mvs-pay/prisma/schema.prisma was cut from 43 models
to roughly 15. 22 models were dropped because Hyperswitch already models the same
concept and is now authoritative:
What stays are the ~11–12 MVS-specific models Hyperswitch does not model:
PayoutMethod, InstantPayoutRequest, PlaidItem (Plaid ACH instant payouts),
Device (POS terminal inventory — Hyperswitch does not manage POS), BillableItem
(healthcare line items), ManualPayment (cash/check), Purchase, Notification,
AdminAuditLog, and CheckoutSession + CheckoutSessionItem.
For data that is MVS-specific but bound to a Hyperswitch entity, the codebase uses sidecar extension tables keyed by Hyperswitch’s opaque ID strings — no foreign key crosses the DB boundary (Hyperswitch’s DB is a separate Neon project):
The pattern: every extension table has a hyperswitch_*_id string column (indexed,
no FK) plus MVS-only columns. Reads join through the SDK, never via SQL across DBs.
The per-MCA variant of this pattern — MerchantConnectorExtension — is its own ADR
(ADR-008).
The eight original phase migrations were squashed to a single baseline at
packages/mvs-pay/prisma/migrations/20260101000000_squashed_baseline/. The originals
are archived (not live) under
packages/mvs-pay/_archive/broken-migrations-pre-squash/. Full schema walkthrough is
in Concepts → Data Model.
ADR-007: the native-Hyperswitch bias
The single principle that governs feature work post-pivot:
Default: surface every Hyperswitch feature through the SDK or UI. Override only via a counter-ADR explaining why an MVS-side rebuild is justified. — ADR-007
The reasoning is the same one that motivated the pivot at all: MVS picked Hyperswitch specifically for its orchestration features, so rebuilding them MVS-side throws away the reason for choosing it and adds permanent maintenance burden. The rejected alternative (“MVS-first: rebuild what we can for stronger control”) loses on exactly that trade.
What this looks like in practice:
- Native features are first-class consumers, not reimplementations. Smart Retries, decision-engine MAB routing, Cost Observability, and Revenue Recovery are configured in Hyperswitch and exposed through the SDK/UI — MVS writes no orchestration logic for them. See Features → Routing & Smart Retries, Features → Revenue Recovery, and Features → Cost Observability.
- MVS-side rebuilds require an explicit ADR. Anything MVS builds itself — custom IIAS substantiation, custom HSA receipts, custom Plaid Link UX — must justify in an ADR why Hyperswitch cannot do it. These exist because they are genuinely MVS-domain-specific (healthcare FSA/HSA, ACH instant payouts), not because rebuilding felt nicer.
When you are about to add a feature, the first question is always: does Hyperswitch already do this? If yes, you wire it through the SDK. If no, you write an ADR.
Why self-hosted at all? (ADR-001)
ADR-001 locks in self-hosting the open-source code on EKS rather than using Juspay’s managed/hosted tier. MVS never uses the managed tier or paid features.
The decisive factor was the trust boundary: hosting moves card data and routing config off MVS infrastructure and gates self-serve features behind a support handshake. The cost is operational ownership — MVS owns the upgrade cadence (see Operations → Hyperswitch Upgrade), DR, and scaling — and the PCI Attestation of Compliance for the card-vault.
The target architecture
A few load-bearing facts about this topology:
- Cards never transit MVS code. The
@mvs/mvs-pay-uiHyperLoader tokenizes card data directly from the browser into the Hyperswitch card-vault. The extensions service only ever sees tokenized references and Hyperswitch IDs. - Two Neon Postgres projects, deliberately separate.
hyperswitch-prodfor Hyperswitch’s router DB;mvs-pay-extensions-prodfor the MVS extensions DB. The no-cross-FK rule above exists precisely because these are different databases. (Neon-over-RDS is ADR-004.) - The SDK→extensions hop is authenticated by an internal shared secret. Full
MVS-tenant auth (
mvs-auth) is deferred — see ADR-010 and Concepts → Authentication & Tenancy. Do not assume per-user auth exists yet at the extensions boundary. - Everything except
apps/mvs-payruns in the EKSpaymentsnamespace. The operator UI deploys to Vercel; the extensions service, thepay-cronsworker, self-hosted Hyperswitch, and the decision-engine all run in EKS. See Infrastructure → Deployment Topology.
The PCI boundary collapse
This is the headline benefit of the pivot.
Before: payfac-gateway was the sole PCI-scoped process
apps/payfac-gateway held raw Finix credentials and brokered every card-data
round-trip. The entire gateway process was in PCI scope.
After: scope collapses to two Hyperswitch containers
The PCI boundary is now just the Hyperswitch card-vault + router containers.
mvs-pay-extensions and the pay-crons worker see only Hyperswitch IDs and
tokenized references — they never hold connector credentials, never receive raw
card data, and are out of PCI scope.
“Out of PCI scope” is a property of the data flow, not a permanent guarantee. It holds only as long as MVS code never touches raw PAN. If you add a flow that makes the extensions service or any MVS app handle raw card data (rather than a HyperLoader token), you pull that surface back into PCI scope and break the AOC. Keep tokenization in the browser, against the vault. Per ADR-001, MVS owns the AOC for the card-vault.
Operational detail — rotation cadence, node-pool isolation, secret paths — lives in Infrastructure → Secrets Management, Operations → Secrets Rotation, and Operations → Security Cadence.
The five connectors behind merchant_connector_account
Hyperswitch’s connector abstraction is what makes Finix “one connector among many.”
A connector is wired to a merchant via a merchant_connector_account (MCA) —
identified by an mca_xxx… ID — which carries the connector’s credentials, enabled
payment methods, and test/prod mode. Five connectors are supported:
Finix’s special status is historical, not architectural. It is the day-1 default
routing target only because it was the incumbent. In the connector model it is just
another row in merchant_connector_account with no privileged position.
How a connector is wired
The hierarchy is merchant_account → business_profile → merchant_connector_account
→ routing rule. Wiring a connector means POSTing its credentials to Hyperswitch’s
admin API (or doing the equivalent in the Control Center UI):
Routing then points traffic at one or more MCAs. Day-1 ships single-connector
routing (algorithm.type: "single", Finix only). Multi-connector priority/fallback
routing across Stripe / Adyen / Cybersource / Braintree, and auth-rate-based dynamic
selection, is the decision-engine’s job:
Step-by-step: create the merchant_account, business_profile, MCA, and routing rule.
Adding a new connector (including Click to Pay as ctp_visa / ctp_mastercard).
Priority routing, decision-engine MAB, and per-connector decline rules.
The sidecar table holding per-MCA MVS metadata.
Click to Pay is not a UI component — it is enabled as connectors (ctp_visa,
ctp_mastercard), so adding it is a connector-onboarding task, not a frontend one.
The full live-API surface for connectors is in the
API Reference tab.
What is honest about gaps
Internal documentation should be honest about loose ends. As of this writing:
- Auth is deferred. The SDK↔extensions boundary uses an internal shared secret;
full
mvs-authtenant integration is a follow-on plan (ADR-010). Treat per-user authorization at the extensions service as not-yet-built. - A handful of M3 schema “design calls” were resolved by recommendation, and some
remain pragmatic choices rather than settled doctrine — notably whether the MVS
billing-platform models stay a separate domain (recommended: separate) and whether
/payment-linksand/onboardingkeep MVS-custom surfaces or defer to Hyperswitch. The built schema reflects the “keep MVS-custom where the workflow differs” stance, but expect to revisit these as Hyperswitch’s payment-link and onboarding surfaces mature. - Validation notes from the May 2026 Hyperswitch v1.123.1 sweep still apply:
Click to Pay V1 (
VisaSRCI) is deprecated in favor of the V2 wrapper; thehyperswitch-stackHelm chart is in maintenance (new deploys usehyperswitch-app-1.1.3); and the decision-engine ships as a standalonejuspay/decision-engineimage with no first-party Helm chart — MVS authors its own underdeploy/decision-engine/. See Operations → Decision Engine.