Architecture Decision Records

ADR index

What an ADR is here

An Architecture Decision Record captures one significant, hard-to-reverse choice: the context that forced the decision, the decision itself, the consequences we accepted, and the alternatives we rejected. They are the “why” behind the platform. If you are taking over MVS Pay, read these before you change anything load-bearing — most of the surprising shapes in the codebase trace back to one of these eleven records.

The source of truth for each ADR is the markdown under docs/architecture/adrs/ in the repo. The pages linked from this index (./adrs/adr-XXX) are the published copies of those same files. This page is the index and the cross-reference map; the linked pages are verbatim.

Every ADR below was Accepted on 2026-05-28 except where the table says otherwise. Several carry an explicit “revisit when…” trigger — those are not closed decisions, they are decisions with a known expiry condition. The triggers are listed in the Revisit triggers section.

The index

ADRTitleDecision in one line
001Hyperswitch self-host vs Juspay-hostedSelf-host the Apache-2.0 Hyperswitch code on EKS; never use Juspay’s managed tier or paid features.
002Hybrid Merchant-of-Record posturePer-tenant MoR by default with an opt-in Platform Organization tier; Phase 1 ships only the isPlatformOrgMember toggle, full Platform Org KYC integration is deferred.
003Hono framework for mvs-pay-extensionsBuild the extensions service on Hono 4.12.23 over HTTP rather than tRPC, because consumers live in multiple repos; the typed SDK provides the type safety.
004Neon Postgres for the extensions DBUse Neon Postgres (two projects: hyperswitch-prod and mvs-pay-extensions-prod) for branching + PITR, not RDS or Aurora; cross-region replica deferred.
005Argo CD for GitOpsReconcile the payments namespace with Argo CD v3.4.2 using a multi-source Application, not Flux or raw Helm-via-CI.
006Extensions as a separate Hono serviceRun orchestration as a standalone apps/mvs-pay-extensions service in the payments namespace rather than inside the Next.js app, to keep a clean PCI boundary and serve cross-repo consumers.
007Maximum native Hyperswitch biasDefault to surfacing every Hyperswitch feature through the SDK/UI; any MVS-side rebuild requires a counter-ADR justifying why upstream can’t do it.
008MerchantConnectorExtension sidecar tableStore per-MCA metadata in a dedicated sidecar table keyed by hyperswitch_merchant_connector_id, not a JSON column, so fields stay indexable and cascade-deletable.
009@mvs/mvs-pay-ui is web-onlyShip web UI only; no iOS/Android/React Native/Flutter wrappers until a real mobile consumer asks for them.
010x-internal-secret auth, mvs-auth deferredUse one shared MVS_PAY_EXTENSIONS_INTERNAL_SECRET for service-to-service trust; per-user identity is not propagated and phiAudit.logAccess() records userId: null until mvs-auth lands.
011Hand off dunning to Revenue RecoveryLet Hyperswitch Revenue Recovery own retry timing and connector selection; MVS keeps subscription state, line items, invoicing, and the recovery UI.

If you only read three, read ADR-007 (the bias that explains why so little is rebuilt MVS-side), ADR-010 (the auth gap you will hit immediately), and ADR-002 (the MoR posture that shapes onboarding and compliance).

How the decisions hang together

ADR-001 is the root: choosing to self-host the open-source Hyperswitch is what makes every downstream decision possible (and necessary). The native-bias principle in ADR-007 is the lens through which most feature work is decided; ADR-011 is the first concrete application of that lens to a feature MVS partly owns.

The grouping by concern:

Per-ADR summary

ADR-001 · Self-host Hyperswitch

Self-host the open-source router on EKS. We own routing config, GSM rules, the decision-engine deployment, upgrade cadence, DR, and the PCI AOC for card-vault. Juspay’s managed tier was rejected to keep the trust boundary on our infrastructure and to make Smart Retries self-serve.

ADR-002 · Hybrid Merchant-of-Record

Per-tenant MoR by default, opt-in Platform Organization tier. Phase 1 ships only the schema toggle (MerchantExtension.isPlatformOrgMember). Full Platform Org KYC API integration is deferred until upstream stabilizes it and product confirms the tier ships. Two compliance trees, two onboarding flows.

ADR-003 · Hono, not tRPC

Hono 4.12.23 for the extensions orchestrator: fast cold start, zod-validator for request schemas, ContentfulStatusCode typing for c.json. tRPC rejected because consumers span repos over HTTP; the typed SDK gives equivalent safety on a stable contract.

ADR-004 · Neon, not RDS

Neon Postgres, two projects (hyperswitch-prod, mvs-pay-extensions-prod). Branchable for staging/preview, PITR built in, PgBouncer pooling required for Vercel cold-start connection storms. Cross-region replica deferred (single-region today). RDS and Aurora Serverless v2 rejected.

ADR-005 · Argo CD, not Flux

Argo CD v3.4.2, multi-source Application (upstream chart + this repo’s values), API version argoproj.io/v1alpha1. Chosen over Flux for UI + multi-source ergonomics; raw Helm-via-CI rejected for lacking drift detection and self-heal.

ADR-006 · Extensions as a separate app

apps/mvs-pay-extensions runs standalone in the payments K8s namespace, not inside the Next.js app. Keeps a clean PCI / non-PCI boundary (only Hyperswitch + card-vault sit inside the PCI ring), serves cross-repo consumers (healthos, mvs-c2), and gets EKS-only features like mTLS and network policies.

ADR-007 · Native-Hyperswitch bias

Default rule: surface every Hyperswitch feature through SDK/UI rather than rebuilding it MVS-side. Any rebuild (custom IIAS substantiation, HSA receipts, Plaid Link UX) needs an explicit counter-ADR. Keeps the MVS surface small; makes Smart Retries, decision-engine, Cost Observability, Revenue Recovery first-class.

ADR-008 · MerchantConnector extension

Per-MCA metadata lives in a sidecar table MerchantConnectorExtension keyed by hyperswitch_merchant_connector_id, not a JSON column on MerchantExtension. Trade-off: slightly more schema surface, but indexable fields (e.g. filter by connectorName) and clean cascading delete.

ADR-009 · Web-only UI SDK

@mvs/mvs-pay-ui ships web only. Hyperswitch’s native iOS/Android/RN/Flutter SDKs are not wrapped. Mobile wrappers are a separate future plan, gated on a real consumer (healthos mobile, an MVS native app, or a partner). Marked “revisit when first mobile consumer requests.”

ADR-010 · Internal secret / deferred auth

Service-to-service trust uses one shared MVS_PAY_EXTENSIONS_INTERNAL_SECRET. Per-user identity is not propagated; phiAudit.logAccess() records userId: null until mvs-auth lands. Mitigations: rate-limit middleware (Phase 3), external WAF, quarterly rotation. JWT validation is added per-route when mvs-auth ships, with a 90-day overlap before decommissioning the secret.

ADR-011 · Revenue Recovery handoff

Hyperswitch Revenue Recovery owns retry timing, BIN-aware curves, and connector selection. MVS keeps the BillingSubscription state machine, line items, invoicing, and the /revenue-recovery UI. New columns recoveryStatus/lastRecoveryAttempt/recoveryAttemptCount (migration 20261001000000_phase4_revenue_recovery) written only by the /webhooks/revenue-recovery handler.

Things these ADRs leave open

This is internal documentation, so the honest version: several ADRs are decisions to defer, not decisions that are done. Treat the items below as known gaps, not finished features.

The following are explicitly deferred or stubbed by the ADRs above. Do not assume they are implemented just because a toggle, column, or webhook exists.

  • Per-user auth is not implemented (ADR-010). Everything authenticates with a single shared secret. PHI audit rows are written with userId: null. The full integration is blocked on the separate mvs-auth plan. See Authentication & Tenancy.
  • Platform Organization tier is a toggle only (ADR-002). MerchantExtension.isPlatformOrgMember exists from Phase 1, but the Platform Organization KYC / sub-merchant onboarding API is deferred until upstream surfaces it stably and product confirms the tier ships. The simple per-tenant connector flow is the only onboarding path live today.
  • No cross-region database replica (ADR-004). Neon is single-region today; cross-region replication is deferred. DR for the extensions DB rests on PITR, not a hot replica.
  • No mobile SDK (ADR-009). Web only. If you are integrating a mobile consumer, that is net-new work governed by a future plan, not an existing wrapper.
  • Revenue Recovery is native and flag-gated (ADR-011). The retry engine is Hyperswitch’s, live behind a feature flag. If it under-performs an MVS baseline (the documented revisit threshold is “recovery rate < industry baseline by 5+ points sustained for 30 days”), the implementation can be swapped under the stable SDK contract. The BillingSubscription recovery columns are written by exactly one writer — the webhook handler at apps/mvs-pay-extensions/src/routes/webhooks-revenue-recovery.ts.

ADR-007’s corollary is procedural: if you find yourself rebuilding a Hyperswitch capability MVS-side, the expectation is that you write a counter-ADR in docs/architecture/adrs/ explaining why upstream can’t carry it, and add a published copy under ./adrs/. The bias is the default, not a hard prohibition.

Revisit triggers

Some ADRs carry an explicit condition under which they should be reopened. Track these — they are the most likely sources of future architecture churn.

ADRRevisit when…
002Upstream Platform Organization API is stable and product confirms the Platform Org tier is shipping — then build the full sub-merchant KYC onboarding flow.
004Multi-region requirements appear — the deferred cross-region replica decision reopens.
009The first real mobile consumer requests it — then a separate plan ships the native/RN wrappers.
010The mvs-auth plan lands — add JWT validation alongside x-internal-secret on every route, then decommission the shared secret over a 90-day overlap.
011Native recovery rate stays 5+ points below an MVS baseline for 30 days — reconsider MVS-side dunning while keeping the SDK contract.

Authoring a new ADR

1

Take the next number

ADRs are sequential. The next record is ADR-012. Numbers are never reused, even if an ADR is superseded.

2

Write the markdown in the repo

Create docs/architecture/adrs/adr-0NN-short-slug.md. Keep the canonical sections: Status, Context, Decision, Consequences, Alternatives considered. ADR-011 also uses a Rationale section where the decision needs more justification — that is fine for substantial decisions.

3

Supersede honestly, don't delete

If a new ADR overrides an old one (for example, a counter-ADR per ADR-007), mark the old one’s Status as superseded and link forward. The history is part of the record.

4

Publish the copy + register it

Add the published copy under fern/pages/concepts/adrs/, add it to this index table and the per-ADR cards above, and register the page in fern/docs.yml under the Architecture Decision Records section.

Canonical source: docs/architecture/adrs/adr-001-*.mdadr-011-revenue-recovery.md.

Published copies (rendered in these docs): fern/pages/concepts/adrs/adr-001-*.mdxadr-011-revenue-recovery.mdx.

Navigation registration: fern/docs.yml, under Core Concepts → Architecture Decision Records.

The published .mdx files are verbatim copies of the source markdown with a Fern frontmatter title added. Keep them in sync — if you edit the source ADR, update the published copy.