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.