Note
This repository is no longer actively maintained and is provided as-is for reference under the MIT License.
Multi-tenant payment orchestration & PayFac settlement API — **Express + Prisma
- Inngest**, deployable to any Node host. Providers: VPS/Payzone (full), Stripe (full), and NAPS (skeleton), plus recurring billing (subscriptions) and BNPL (installments).
flowchart TB
subgraph Clients
Web["CorpoPay Web"]
Checkout["Hosted checkout"]
S2S["Server-to-server API"]
end
subgraph Core["CorpoPay API (Express)"]
Routes["Routes + middleware"]
Adapters["Provider adapters"]
Settlement["Settlement engine"]
Webhooks["Webhooks"]
end
Web --> Routes
Checkout --> Routes
S2S --> Routes
Routes --> Adapters
Routes --> Settlement
Webhooks --> Routes
Adapters --> Stripe["Stripe"]
Adapters --> VPS["VPS / Payzone"]
Adapters --> NAPS["NAPS"]
Routes --> DB[("PostgreSQL (Prisma)")]
Settlement --> DB
Routes --> Jobs["Inngest jobs"]
Jobs --> DB
- Multi-tenancy — tenants are fully isolated; every tenant-scoped query filters
by
tenantIdtaken from the authenticated user, never from a client-supplied value. - Hosted payment links and a server-to-server API (
/payment-intents). - Provider adapters behind a single
ProviderAdapterinterface (src/adapters/) — add a PSP without touching any route. - Recurring billing (subscriptions, dunning) and BNPL installments.
- PayFac settlement — a double-entry money ledger (7 accounts), per-tenant fee schedules + settlement policies, payouts, and chargeback/reversal clawback with recoveries.
- Webhooks with synchronous signature verification and idempotent dedup.
- Generated OpenAPI contract (
src/openapi.ts) — the single source of truth shared with the web app.
Prerequisites: Node 24+, Docker.
cp .env.example .env # fill in DATABASE_URL, JWT_SECRET, ENCRYPTION_KEY, …
docker compose up --build # API :4000 · Inngest dev server :8288docker compose up runs migrations and the seed automatically. See
.env.example for the full list of variables.
npm install
npx prisma migrate dev
npm run dev # :4000A production image is published to ghcr.io/corpopay/corpopay-api on every v*
tag (multi-arch, Trivy-scanned). Run it against your own Postgres:
docker run --rm -p 4000:4000 \
-e DATABASE_URL="postgresql://…" \
ghcr.io/corpopay/corpopay-api:latestThe container runs prisma migrate deploy on start and serves the API on :4000.
npm run typecheck
npm run lint
npm run test| Variable | Description |
|---|---|
DATABASE_URL |
PostgreSQL connection string (pooled) |
DIRECT_URL |
Direct connection string (migrations only) |
JWT_SECRET / JWT_EXPIRES_IN |
Auth signing secret + token expiry |
ENCRYPTION_KEY |
64-char hex (32 bytes) — AES-256-GCM |
INNGEST_EVENT_KEY / INNGEST_SIGNING_KEY |
Inngest event + signing keys |
API_BASE_URL / WEB_BASE_URL |
Public URLs (callbacks, checkout links) |
Secrets come from environment variables only — never hardcode them.
- Money — requests are centimes (int); the database stores MAD
Decimal(12,2); responses arenumber | string | null. Convert only viasrc/lib/money.ts. Never write a bare/ 100or* 100. - Statuses — Prisma enums are the single source of truth.
CANCELLED(provider/webhook) andCANCELED(PaymentIntentStatus) are both real and different — do not collapse them. Provider→internal mapping lives insrc/lib/status-maps.ts.
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/public/checkout/:slug |
Fetch payment link |
POST |
/public/checkout/:slug/pay |
Initiate payment |
GET |
/public/installment-plans/:slug |
BNPL plan preview |
GET |
/public/pay/:correlationId |
Paywall relay page |
| Method | Path |
|---|---|
POST |
/webhooks/naps |
POST |
/webhooks/vps |
POST |
/webhooks/stripe |
POST |
/api/inngest |
| Method | Path |
|---|---|
POST |
/auth/register |
POST |
/auth/login |
GET |
/auth/me |
POST |
/auth/forgot-password |
POST |
/auth/reset-password |
| Method | Path |
|---|---|
GET/PATCH |
/tenant |
GET/POST |
/users (+ POST /invite, PATCH /:id/role, DELETE /:id) |
GET/POST |
/provider-configs (+ POST /:id/test, PATCH /:id/status, DELETE /:id) |
GET/POST |
/payment-links (+ GET /:id, PATCH /:id/cancel) |
POST |
/payment-intents (+ GET /:id, capture/cancel/status) |
GET |
/transactions, /transactions/:id |
POST |
/transactions/:id/refund |
GET |
/dashboard/summary |
GET |
/exports/transactions.csv |
GET/POST |
/api-keys (+ DELETE /:id) |
GET |
/subscriptions (+ pause/resume/cancel/events) |
GET/POST/PATCH/DELETE |
/installment-plans |
GET |
/installment-agreements (+ POST /:id/cancel) |
| Method | Path |
|---|---|
GET |
/ledger |
GET/POST |
/fee-schedules (+ GET /active) |
GET/POST |
/settlement-policies (+ GET /active) |
GET/POST |
/payouts (+ GET /:id, POST /:id/cancel, POST /:id/process) |
GET/POST |
/disputes (+ GET /:id, POST /:id/resolve) |
| Method | Path |
|---|---|
GET |
/admin/tenants, /admin/tenants/:id |
PATCH |
/admin/tenants/:id/status |
GET |
/admin/payments/search |
GET |
/admin/webhooks |
GET/PUT |
/admin/provider-health |
GET/POST |
/admin/simulation |
POST |
/admin/payouts/:id/execute (manual payout) |
| Layer | Technology |
|---|---|
| Runtime | Node.js 24, Express |
| ORM | Prisma 7 + PostgreSQL |
| Background jobs | Inngest |
| Auth | JWT (jsonwebtoken), bcryptjs |
| Encryption | AES-256-GCM (Node crypto) |
| Deploy | Docker Compose (or any Node host) |
MIT — see LICENSE.
See CONTRIBUTING.md.