Local Development

Run it on your machine

This page is the ground-truth local setup for mvs-pay: prerequisites, install, Prisma client generation, the type-check / test / build loop, running the two apps, the env vars each one needs, and the changeset workflow you must follow when you touch a published package.

The authoritative sources are CONTRIBUTING.md and README.md at the repo root. This page reconciles those with what the code in apps/mvs-pay-extensions/src/config/ and the various package.json scripts actually do — and flags the few places they disagree.

After the Hyperswitch pivot, MVS application code is out of PCI scope — raw connector credentials live only inside Hyperswitch’s merchant_connector_account config. Your laptop never holds card data or connector secrets. See The Hyperswitch Pivot and Authentication & Tenancy for the why.

What you are running

The monorepo ships 2 apps plus a Temporal worker. For day-to-day UI/API work you typically run the first two.

SurfaceFilterPortEntry
Merchant + operator UI (Next.js 16)mvs-pay3002next dev --port 3002 --turbo
Hyperswitch orchestration service (Hono)mvs-pay-extensions3004 (see warning)tsx watch src/dev.tssrc/server.ts
pay-crons Temporal worker@mvs/pay-temporal-workflowsn/a (worker)tsx src/worker/start.ts

Port mismatch — read this. CONTRIBUTING.md and README.md both say the extensions service runs on :3005, but the dev script (tsx watch src/dev.ts) has no --port flag, so the Hono server binds to the config default. That default is 3004 in apps/mvs-pay-extensions/src/config/index.ts (port: z.coerce.number().default(3004)), matching apps/mvs-pay-extensions/.env.example (PORT=3004) and the Dockerfile (EXPOSE 3004 / ENV PORT=3004).

To actually get :3005, set PORT=3005 in the extensions env, and point the UI at it with MVS_PAY_EXTENSIONS_URL=http://localhost:3005. Whatever port you pick, the UI’s MVS_PAY_EXTENSIONS_URL must match it. This page uses 3005 to match the docs; substitute 3004 if you leave PORT unset.

Prerequisites

RequirementVersionSource of truth
Node>=24.nvmrc (24), root package.json engines.node
pnpm11.4.0root package.json packageManager + engines.pnpm >=11.4.0
corepackbundled with Nodeactivates the pinned pnpm
Dockerany recentlocal Hyperswitch + Postgres + Redis stack
OSmacOS / LinuxWindows via WSL2

The repo pins the exact pnpm build via packageManager (with an integrity hash), so always go through corepack rather than a globally-installed pnpm — a mismatched pnpm can produce a different lockfile resolution.

Step-by-step setup

1

Activate the pinned pnpm via corepack

Node 24 ships corepack. Enable it and pin the exact pnpm the repo expects:

$corepack enable
$corepack prepare pnpm@11.4.0 --activate

Verify: node -v should report v24.x (use nvm use if you have nvm — it reads .nvmrc), and pnpm -v should print 11.4.0.

2

Clone and install

$git clone https://github.com/MVS-CLOUD/mvs-pay.git
$cd mvs-pay
$
$pnpm install --frozen-lockfile

--frozen-lockfile is what CI uses; matching it locally avoids “works on my machine” lockfile drift. Workspace packages are linked via the workspace:* protocol, so the two apps resolve @mvs/mvs-pay-sdk, @mvs/mvs-pay-ui, @mvs/mvs-pay, etc. from the local tree, not the registry.

3

Generate the dedicated mvs-pay Prisma client

Both apps depend on the generated @prisma/mvs-pay-client, which is not checked in — it is a link: into node_modules. Generate it before type-checking or building, or tsc will fail on missing model/enum types:

$pnpm --filter @mvs/mvs-pay db:generate

This runs prisma generate --config=./prisma.config.ts in packages/mvs-pay/.

You normally don’t need to run this by hand before a build: both apps declare a prebuild hook (pnpm --filter @mvs/mvs-pay db:generate) that regenerates the client first. Run it manually after a fresh install, after pulling schema changes in packages/mvs-pay/prisma/, or before the first type-check. MVS_PAY_DATABASE_URL must be set for generation — see Env vars.

4

Configure environment variables

Copy the example files and fill them in from Infisical (never paste secrets into a committed file):

$cp apps/mvs-pay/.env.example apps/mvs-pay/.env.local
$cp apps/mvs-pay-extensions/.env.example apps/mvs-pay-extensions/.env.local

See Required environment variables below for what each one is and where it comes from.

5

Run the green-build loop once

Confirm the workspace is healthy before you start changing things:

$pnpm turbo run type-check # tsc --noEmit across every package
$pnpm turbo run test # vitest where defined
$pnpm turbo run build # build every package + app

The Turborepo task graph makes build wait on its workspace dependencies and test wait on build where a test needs the compiled dist/.

6

Start the apps

In separate terminals:

$# Terminal 1 — merchant + operator UI on :3002
$pnpm --filter mvs-pay dev
$
$# Terminal 2 — Hyperswitch orchestration service (see port warning above)
$PORT=3005 pnpm --filter mvs-pay-extensions dev
$
$# Terminal 3 (optional) — pay-crons Temporal worker against a local Temporal
$pnpm --filter @mvs/pay-temporal-workflows start:worker:dev

Then open http://localhost:3002. The extensions service exposes a /health check (see the Dockerfile healthcheck against /health) — curl http://localhost:3005/health to confirm it booted.

Required environment variables

Secrets come from Infisical, not hand-edited .env files in production. For local dev you fill *.env.local from the dev scope. The canonical runtime reader is @mvs/secrets. The full catalog lives at Reference → Environment Variables and in Secrets Management; this section is the minimum to boot locally.

apps/mvs-pay (the UI)

Minimum to boot, per CONTRIBUTING.md and apps/mvs-pay/.env.example:

VariablePurpose
MVS_PAY_DATABASE_URLThe dedicated payment Postgres (Neon) this repo owns. Required for Prisma generate at build time and at runtime.
CONTROL_PLANE_DATABASE_URLControl-plane DB, read via the @mvs/control-plane peer.
MVS_PAY_EXTENSIONS_URLBase URL of the extensions service. Must match the port you actually ran it on — http://localhost:3005 (or :3004).
MVS_PAY_EXTENSIONS_INTERNAL_SECRETShared service-to-service secret the SDK sends as x-internal-secret. Use a dev-only random string.
NEXT_PUBLIC_HYPERSWITCH_PUBLISHABLE_KEYPer-merchant Hyperswitch publishable key (pk_...) surfaced to the browser by @mvs/mvs-pay-ui.
BETTER_AUTH_SECRET / BETTER_AUTH_URLBetter Auth (placeholder locally; mvs-auth owns this in prod).
NEXT_PUBLIC_AUTH_URL, NEXT_PUBLIC_BASE_DOMAINAuth client + domain wiring.
INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET / INFISICAL_PROJECT_IDMachine identity for the Infisical reader.
INFISICAL_ENVScope selector — set to dev locally.

apps/mvs-pay-extensions (the Hono service)

The env contract is apps/mvs-pay-extensions/.env.example, validated and defaulted by the Zod schema in apps/mvs-pay-extensions/src/config/index.ts:

VariableEnv keyDefault in schemaNotes
PortPORT3004Set 3005 to match the docs (see warning).
Node envNODE_ENVdevelopmentdevelopment | staging | production.
Hyperswitch base URLHYPERSWITCH_API_BASE_URLhttps://hyperswitch.payments.internalMust be a valid URL. The default is the in-cluster host and is not reachable from a laptop — override it locally (see Hyperswitch below).
Hyperswitch admin keyHYPERSWITCH_ADMIN_API_KEY(optional)Required only for privileged ops (merchant_account, merchant_connector_account, routing). isHyperswitchConfigured() is false without it.
Hyperswitch webhook secretHYPERSWITCH_WEBHOOK_SECRET(optional)Verifies inbound Hyperswitch webhooks.
Internal secretMVS_PAY_EXTENSIONS_INTERNAL_SECRET(optional)Must equal the UI’s value. See internal secret.
Platform-admin secretMVS_PAY_EXTENSIONS_PLATFORM_ADMIN_SECRET(optional)Elevated operator-only routes.
Payment DBMVS_PAY_DATABASE_URLSame DB as the UI.
Control-plane DBCONTROL_PLANE_DATABASE_URL(optional)
RedisREDIS_URL(optional)Cache + rate limiting.
InfisicalINFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET / INFISICAL_PROJECT_ID(optional)isInfisicalConfigured() requires all three.
Infisical scopeINFISICAL_ENVdev (in .env.example)See honest note below.
PlaidPLAID_CLIENT_ID / PLAID_SECRET / PLAID_ENVPLAID_ENVsandboxPhase 5 / M24 instant payouts; optional locally.
DatadogDD_API_KEY / DD_SERVICE / DD_ENVOptional tracing via dd-trace.

INFISICAL_ENV appears in .env.example and CONTRIBUTING.md but is not read by the extensions config schema — the schema only consumes INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, and INFISICAL_PROJECT_ID. INFISICAL_ENV is consumed by the Infisical CLI itself (pnpm dlx @infisical/cli run --env=dev) to pick which scope to inject. Treat it as a CLI/runtime selector, not an app config value.

The internal secret (x-internal-secret)

MVS_PAY_EXTENSIONS_INTERNAL_SECRET is the only authentication between the UI (and every other consumer) and the extensions service. Per ADR-010, per-user auth via mvs-auth is deferred: there is a single shared secret, no propagated user identity, and phiAudit.logAccess() records userId: null until mvs-auth lands. Locally:

  • Use any random dev-only string.
  • It must be identical in apps/mvs-pay/.env.local and apps/mvs-pay-extensions/.env.local, or every SDK call gets rejected.
  • In production it is rotated quarterly (ADR-010) / weekly per the pivot doc; that doesn’t matter locally beyond keeping the two values in sync.

Hyperswitch base URL

The default https://hyperswitch.payments.internal is the in-cluster DNS name and will not resolve from your machine.

For local work, point HYPERSWITCH_API_BASE_URL at a Hyperswitch sandbox environment (never production — per the PCI rules in CONTRIBUTING.md), or run the local Docker Hyperswitch stack and point at it. Pull sandbox keys only via pnpm dlx @infisical/cli run --env=dev — do not copy them into a .env. Many extension routes that need privileged Hyperswitch operations will no-op or error until HYPERSWITCH_ADMIN_API_KEY is set (isHyperswitchConfigured() gates them).

Common workflows

$pnpm turbo run type-check # tsc --noEmit, every package
$pnpm turbo run test # vitest where defined
$pnpm turbo run build # build every package + app
$
$pnpm --filter mvs-pay dev # Next.js merchant + operator UI on :3002
$pnpm --filter mvs-pay-extensions dev # Hono orchestration service (PORT=3005 to match docs)
$pnpm --filter @mvs/pay-temporal-workflows start:worker:dev # pay-crons worker
$
$pnpm --filter @mvs/mvs-pay-sdk build # one package only
$pnpm --filter @mvs/mvs-pay-ui test
$
$pnpm check # biome lint + format check
$pnpm format # biome format --write

Database helper scripts (packages/mvs-pay)

The dedicated Prisma client package exposes the DB workflow scripts. All use --config=./prisma.config.ts and operate against MVS_PAY_DATABASE_URL:

ScriptCommand
db:generateprisma generate — regenerate @prisma/mvs-pay-client.
db:pushprisma db push — push schema without a migration (scratch/dev only).
db:migrateprisma migrate dev — create + apply a dev migration.
db:migrate:deployprisma migrate deploy — apply pending migrations (prod path).
db:migrate:statusprisma migrate status.
db:studioprisma studio — browse the DB.

Migrations in packages/mvs-pay/prisma/migrations/ are forward-only and Tier 0 / behavior-parity critical. No DROP COLUMN / DROP TABLE against existing prod data without a multi-stage plan attached to the PR (CONTRIBUTING.md). The payment DB schema and the MVS_PAY_EXTENSIONS_INTERNAL_SECRET validation path require @MVS-CLOUD/payments review. See Data Model.

Pre-commit checks

CI is the source of truth (the Buildkite pipeline), but run these locally before you push — a green local run is necessary, not sufficient:

$pnpm check # biome lint + format
$pnpm turbo run type-check
$pnpm turbo run test
$pnpm turbo run build

Changesets — required for published packages

@mvs/mvs-pay-sdk and @mvs/mvs-pay-ui are the only published packages. Per .changeset/config.json they are a linked release group — they version together — published restricted to the Buildkite Package Registry, with baseBranch: main and commit: false (you commit the changeset file yourself).

1

Run the CLI

$pnpm changeset

It prompts for: which packages changed, the semver bump (patch / minor / major), and a user-facing summary that lands in the published CHANGELOG.md.

2

Commit the generated file

The CLI writes a markdown file into .changeset/. Commit it as part of your PR. CI surfaces pnpm changeset status --verbose so reviewers see exactly what would publish.

When you do NOT need a changeset

Per CONTRIBUTING.md and the ignore list in .changeset/config.json:

  • Changes confined to apps/* — both apps are private/unpublished.
  • The transitional vendored @mvs/* packages (@mvs/cache, @mvs/config, @mvs/env-config, @mvs/i18n, @mvs/logs, @mvs/mail, @mvs/secrets, @mvs/shared, @mvs/ui, @mvs/utils) — pinned to 0.1.0 until mvs-packages publishes; they do not bump from here.
  • packages/mvs-pay/ — the dedicated Prisma client is workspace-only.
  • @mvs/pay-temporal-workflows, @mvs/tailwind-config, @mvs/tsconfig — ignored.
  • .buildkite/, .github/, docs/, or root config files.

Where to go next