MVS Pay

Internal engineering documentation

This is the internal engineering documentation for MVS Pay — the payment platform for MVS Cloud Holdings. It is written for the engineer taking over the platform: it is precise about what is built, and honest about what is stubbed, deferred, or not yet provisioned.

State of the world (as of the 2026-05-29 handoff). All planned work — Phases 0–5, milestones M0–M28 — is built and merged to main, followed by two rounds of gap-audit remediation. The repo reports green locally (677 tests). Nothing is deployed yet. The remaining gap to “working in production” is almost entirely infrastructure, secrets, external-account provisioning, and a domain cutover — not application code. See Deployment Readiness.

What MVS Pay is

MVS Pay is a payment-orchestration platform, built on a self-hosted deployment of Hyperswitch — Juspay’s open-source (Apache-2.0) payment orchestration platform, pinned to v1.123.1. Hyperswitch is the source of truth for payment_intent, payment_attempt, refund, dispute, mandate, customer, payment_method, merchant_account, connector configuration, and routing rules. MVS code wraps it, adds the MVS-specific features Hyperswitch does not model, and exposes everything to other MVS apps as two published packages.

The product framing is important: MVS Pay is a package other MVS apps consume. Consumers (healthos, mvs-c2, and apps/mvs-pay itself) talk to payments only through the published @mvs/mvs-pay-sdk — a typed server-side Node HTTP client. They never touch Hyperswitch directly.

The platform was pivoted from a previous Finix-direct architecture. The deleted apps/payfac-gateway, the @mvs/payments Finix client, packages/payfac-jobs, and the 43-table Finix Prisma schema are gone. Finix is now just one connector among five (Stripe, Adyen, Cybersource, Braintree, Finix), plugged in through Hyperswitch’s merchant_connector_account surface. The pre-hyperswitch-pivot git tag marks the last commit before the pivot, for recovery.

The single defining win of the pivot is the PCI boundary collapse: the PCI ring narrows to just the Hyperswitch router + card-vault containers. Cards are tokenized client-side via Hyperswitch’s HyperLoader directly into the vault, so apps/mvs-pay, apps/mvs-pay-extensions, the pay-crons worker, and both published packages only ever see Hyperswitch IDs and tokenized references — never raw PANs, never connector credentials. They are out of PCI scope.

The big picture

MVS Pay is a five-layer system. Requests flow from a consumer app, through the SDK, into the MVS orchestration service, and out to Hyperswitch and its connectors. Card data takes a separate browser-side path straight into the vault.

LayerComponentRole
1. Consumershealthos, mvs-c2, apps/mvs-payCall payments only through @mvs/mvs-pay-sdk. Never touch Hyperswitch.
2. UIapps/mvs-payNext.js 16 merchant + operator dashboard. Today targets pay.mvscloud.com (dev :3002). Out of PCI scope.
3. Browser checkout@mvs/mvs-pay-uiWeb-only React components wrapping Hyperswitch’s HyperLoader. Tokenizes cards directly into the vault.
4. Orchestrationapps/mvs-pay-extensionsHono service on EKS (payments namespace, dev :3005). The only thing that calls Hyperswitch. Wraps its REST API, persists MVS extension data in its own Neon DB via Prisma. Out of PCI scope.
5. Source of truthHyperswitch + connectorsThe payment engine and sole PCI surface. Fronts five connectors plus the decision-engine MAB router.
Asyncpay-crons Temporal workerWebhook DLQ drainer, instant-payout settlement, residual estimation, dispute SLA, device health, success-rate reconcile.
Datapackages/mvs-payDedicated Prisma client (@prisma/mvs-pay-client) for the extensions DB. ~15 MVS-specific models + sidecar extension tables keyed by opaque Hyperswitch IDs (no cross-DB FK).

The guiding architectural principle is maximum native-Hyperswitch bias (ADR-007): surface every Hyperswitch feature through the SDK or UI, and require a counter-ADR before rebuilding anything MVS-side. As a result, Smart Retries, decision-engine MAB routing, Cost Observability, and Revenue Recovery / dunning are all delegated to Hyperswitch. MVS keeps only what Hyperswitch does not model: Plaid-backed instant payouts, POS device inventory, residuals, the custom billing platform, and subscription state.

Repository shape

A pnpm + Turbo monorepo. Two apps and 14 packages (four first-party, ten transitional vendored copies of mvs-packages foundations pinned at 0.1.0).

SurfacePathPurpose
mvs-payapps/mvs-pay/Next.js 16 merchant + operator UI at pay.mvscloud.com.
mvs-pay-extensionsapps/mvs-pay-extensions/Hono service wrapping Hyperswitch + persisting MVS extension data.
@mvs/mvs-pay-sdkpackages/mvs-pay-sdk/Published typed Node HTTP client for mvs-pay-extensions. The only payments entrypoint for consumers.
@mvs/mvs-pay-uipackages/mvs-pay-ui/Published React checkout components wrapping HyperLoader (web-only, ADR-009).
@mvs/mvs-paypackages/mvs-pay/Dedicated Prisma client + schema for the extensions DB (MVS_PAY_DATABASE_URL).
@mvs/pay-temporal-workflowspackages/temporal-workflows/The pay-crons task-queue worker, activities, and workflows.

The ten vendored @mvs/* packages (@mvs/cache, @mvs/config, @mvs/env-config, @mvs/i18n, @mvs/logs, @mvs/mail, @mvs/secrets, @mvs/shared, @mvs/ui, @mvs/utils) are transitional copies of foundations that will live in mvs-packages. When upstream publishes, this repo deletes the local copy and flips workspace:*^x.y.z.

Toolchain at a glance

ToolVersion / note
Node>=24 (see .nvmrc)
pnpm11.4.0 (root packageManager)
Turbotask graph + remote cache
Biomelint + format
Changesetslinked release group: @mvs/mvs-pay-sdk + @mvs/mvs-pay-ui
Hyperswitchv1.123.1, self-hosted on EKS
Temporalpay-crons task-queue worker
Better Authconsumed via @mvs/auth-client from mvs-auth
$corepack enable
$corepack prepare pnpm@11.4.0 --activate
$
$pnpm install --frozen-lockfile
$pnpm turbo run type-check
$pnpm turbo run test
$pnpm turbo run build
$
$pnpm --filter mvs-pay dev # merchant + operator UI on :3002
$pnpm --filter mvs-pay-extensions dev # Hyperswitch orchestration service on :3005

Start here

Honest accounting of gaps

This is internal documentation; accuracy beats polish. The platform is built and coherent, but a new owner should know exactly where the edges are.

The single most important fact: nothing is deployed, and the GitOps layer cannot reconcile as-is. Two verified hard blockers gate the EKS half of the system:

  • Every Argo CD Application points at the wrong Git repo. All five manifests under deploy/argo/*-application.yaml use repoURL: https://github.com/abzholdings/mvs-pay.git. If the real repo is under MVS-CLOUD, Argo CD syncs nothing. One-line-per-file fix; total blocker.
  • Kustomize image references are placeholders. The extensions and pay-crons-worker deployment bases and prod overlays carry literal REPLACE_ME_REGISTRY / REPLACE_ME_IMAGE_SHA / AWS_ACCOUNT_ID strings.

See Deployment Readiness for the full, prioritized checklist.

Deferred by ADR (decisions, not defects)

  • Auth — the one with real security weight. apps/mvs-pay-extensions trusts a single shared MVS_PAY_EXTENSIONS_INTERNAL_SECRET (x-internal-secret). Per-user JWT validation via mvs-auth is deferred (ADR-010). Until it lands, an upstream consumer compromise has full payments blast radius, and phiAudit.logAccess records organizationId but a null userId.
  • Mobile SDKs — web-only by decision (ADR-009). No iOS/Android/RN wrappers.
  • Hybrid Merchant-of-Record Platform-Org tier — the opt-in toggle (MerchantExtension.isPlatformOrgMember) ships, but the full Hyperswitch Platform-Organization sub-merchant KYC integration is deferred (ADR-002). There is no real KYC gate today: “approved” is inferred from any ENABLED connector.

Honest 501 stubs (decide launch scope)

These routes return an explicit HTTP 501 today, not a fake success:

  • Terminal device lifecycledevices/[id]/{activate,action,idle-image,idle-image/upload}. Devices is list/read-only; merchants cannot control physical POS terminals.
  • Hosted checkout + self-serve billing portalbilling/checkout-sessions and billing/customers/[id]/portal-session.
  • Merchant KYC remediationonboarding/{remediation,resubmit} (tied to the deferred MoR tier above).

Other things to know

  • Cost Observability shows real numbers only if the OLAP profile (Kafka + ClickHouse) is provisioned; otherwise it returns a deep-link and renders an empty state.
  • CI under-tests the UI. The Buildkite pipeline runs turbo run test --filter="!mvs-pay" --filter="!mvs-pay-extensions" (extensions has its own step); there is no mvs-pay app test step. The “677 tests passing” figure is a local full run, not a CI guarantee. Both Playwright e2e suites are non-blocking (soft_fail / test.skip on unset sandbox creds).
  • Finix vocabulary persists as a drift trap. Field names like finixMerchantId / finixEnabled now carry Hyperswitch IDs; apps/mvs-pay/lib/api-client.ts is still a full Finix error-code map.
  • Cross-repo cutovers are pending. mvs-c2’s apps/platform-admin still imports the deleted @mvs/payfac-client and is broken until cut over; healthos must move to @mvs/mvs-pay-sdk@1.0.0 (a ./legacy shim subpath covers a 30-day overlap). See the cutover runbooks under Operations.

Key decisions (ADR index)

The architecture is governed by eleven ADRs under Concepts → ADRs:

ADRDecision
001Self-host Hyperswitch OSS on EKS (not the Juspay managed tier).
002Hybrid Merchant-of-Record; Platform-Org KYC deferred.
003Hono (not tRPC) for mvs-pay-extensions.
004Neon Postgres (not RDS/Aurora); two projects.
005Argo CD (not Flux) for GitOps.
006Extensions is a separate Hono service, not Next.js API routes.
007Maximum native-Hyperswitch bias.
008MerchantConnectorExtension sidecar table.
009@mvs/mvs-pay-ui is web-only.
010Shared internal secret; per-user auth deferred.
011Hand dunning/retry orchestration to Hyperswitch Revenue Recovery.

Reference