Local Development
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.
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
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
Activate the pinned pnpm via corepack
Node 24 ships corepack. Enable it and pin the exact pnpm the repo expects:
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.
Clone and install
--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.
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:
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.
Configure environment variables
Copy the example files and fill them in from Infisical (never paste secrets into a committed file):
See Required environment variables below for what each one is and where it comes from.
Run the green-build loop once
Confirm the workspace is healthy before you start changing things:
The Turborepo task graph
makes build wait on its workspace dependencies and test wait on build where a
test needs the compiled dist/.
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:
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:
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.localandapps/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
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:
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:
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).
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 to0.1.0untilmvs-packagespublishes; 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.
Why is @prisma/mvs-pay-client a link: dependency, not a normal package?
The generated Prisma client is built into node_modules/@prisma/mvs-pay-client and
referenced from both apps and packages/mvs-pay via
link:../../node_modules/@prisma/mvs-pay-client. It is generated, never published, and
not committed — which is exactly why db:generate must run after a fresh install and
why both apps gate their builds behind a prebuild that regenerates it. For the
cross-repo Prisma generation story (peer clients like @prisma/control-plane-client),
see Cross-repo Prisma Build.
Where to go next
The apps, packages, and which ones are published vs vendored.
How the UI, extensions service, Hyperswitch, and the worker fit together.
The Hono service you just ran on :3004/:3005, route by route.
The full env var catalog beyond the local-boot minimum.
Infisical paths, the @mvs/secrets reader, and PCI handling rules.
The extensions HTTP API consumed via @mvs/mvs-pay-sdk.