A production-oriented MVP backend for a marketplace where one customer order can contain products from multiple independently operated vendors. It is built with NestJS, TypeScript, PostgreSQL, Prisma, Redis, BullMQ, REST, OpenAPI, and self-hosted ZITADEL authentication.
New to the repository? Follow the Fresh Clone Starter Guide for system requirements, environment setup, Docker and non-Docker installation, database initialization, testing, GitHub Actions CI, and a safe CD release blueprint.
The application is a modular monolith: one deployable API with explicit internal domain boundaries. PostgreSQL owns transactional truth; Redis supports retryable/delayed jobs and is never used as the commerce system of record.
- ZITADEL Hosted Login, registration/recovery/MFA-ready identity flows, opaque-or-JWT access-token introspection, and just-in-time local commerce identities;
- ZITADEL capability roles with exact organization-scoped authorization plus vendor resource ownership isolation;
- vendor application/verification and audited administrative lifecycle actions;
- category, attribute, product, variant, media-reference, moderation and inventory workflows;
- public product discovery, single-currency multi-vendor carts, server-authoritative pricing, constrained promotions and idempotent checkout;
- concurrency-safe inventory reservation, confirmation-time expiry validation, delayed release, commit and auditable movement history;
- parent marketplace orders split into independently fulfilled vendor orders with immutable item snapshots;
- verified/idempotent mock payment webhook handling, commission snapshots, append-oriented vendor ledger and settlement lifecycle;
- mock shipping/AWB/tracking, one-active-forward-shipment enforcement, normalized tracking events and retry-safe delivery convergence;
- basic return, partial/full refund with ledger/eligible-settlement rebuilding, dispute and verified-purchase review paths;
- queued/logging notifications, provider ports, audit logs, health checks, request IDs and consistent errors;
- Prisma 7 migrations, explicit generated client, PostgreSQL driver adapter and seed data, Docker infrastructure, tests, Swagger and CI.
Advanced KYC, search ranking, tax, complex promotion/funding rules, fraud, helpdesk synchronization, real payment/payout/shipping rails, and production notification/media transports remain explicit integration work; see Known limitations.
flowchart LR
Clients[Customer / Vendor / Admin clients]
IAM[Self-hosted ZITADEL]
API[One NestJS REST application]
Modules[Bounded marketplace modules]
DB[(PostgreSQL)]
Redis[(Redis / BullMQ)]
Providers[Mock/provider adapters]
Clients --> IAM
IAM --> Clients
Clients --> API
API --> IAM
API --> Modules
Modules --> DB
Modules --> Redis
Modules --> Providers
Synchronous commerce rules use direct application-service calls and database transactions. Committed domain events trigger side effects; BullMQ handles work that benefits from retry or delay. Provider-specific behavior is behind application-owned interfaces.
Detailed design:
- Fresh clone, Docker, testing, and CI/CD starter guide
- Architecture and transaction boundaries
- Module ownership map
- Database and persistence
- API conventions
- Checkout, order splitting, and lifecycle
- Payments, commission, ledger, refunds, and settlement
- Shipping and tracking
- Integrations and jobs
- Security model and launch checklist
- ZITADEL authentication, self-hosting, provisioning, and migration
- ZITADEL change blueprint and rollout boundary
- Node.js 20.19 or later (Node.js 22 LTS is used by Docker and CI)
- Corepack and pnpm 10.14
- Docker Engine with Docker Compose v2
- Terraform 1.8+ for repeatable ZITADEL resource provisioning
- PostgreSQL 16 and Redis 7 if not using Compose
corepack enable
pnpm install --frozen-lockfileCreate a local environment file:
# macOS/Linux
cp .env.example .env
# PowerShell
Copy-Item .env.example .envFirst start and provision ZITADEL. Replace the obvious secrets in its .env before the first start:
cd infra/zitadel
cp .env.example .env
mkdir -p bootstrap
docker compose --env-file .env -f compose.yml up -d --wait
cd terraform
cp terraform.tfvars.example terraform.tfvarscd ../../..
pnpm zitadel:terraform:init
pnpm zitadel:terraform:applyor
cd infra/zitadel/terraform
terraform init
terraform apply -parallelism=1The repository command deliberately applies ZITADEL resources with Terraform parallelism set to one. Project roles share a ZITADEL event aggregate and concurrent creation can otherwise exhaust ZITADEL's event-write retries. PowerShell equivalents and recovery guidance are documented in ZITADEL Authentication and Self-Hosting. Copy the Terraform IDs and sensitive client IDs into the root .env; generated runtime keys are written under the ignored secrets/zitadel directory.
Then start PostgreSQL and Redis, apply checked-in marketplace migrations, seed fake commerce data, and run the API from the repository root:
docker compose up -d
pnpm prisma:generate
pnpm migrate:deploy
pnpm seed
pnpm start:devpnpm seed also runs prisma migrate deploy before inserting development data. This makes the command safe to rerun after pulling new checked-in migrations; it does not reset the database.
Then open:
- Swagger UI: http://localhost:3000/docs
- OpenAPI JSON: http://localhost:3000/docs/openapi.json
- Liveness: http://localhost:3000/api/v1/health/live
- Full dependency readiness: http://localhost:3000/api/v1/health/ready
- ZITADEL Console: http://auth.localhost:8080/ui/console/
pnpm migrate:dev -- --name <change_name> is for authoring a new local migration. Normal startup and deployment use pnpm migrate:deploy; never substitute prisma db push in production.
The canonical, deterministic API snapshot is checked in at
openapi/openapi.json. Run pnpm openapi:generate after changing controllers,
DTOs, or Swagger metadata. pnpm openapi:check reconstructs the same document
without starting a listener or contacting PostgreSQL, Redis, or ZITADEL, and CI
fails when the snapshot is stale.
The application profile builds a one-shot migration image and the unprivileged production API image. PostgreSQL, Redis, and the separately managed ZITADEL stack must be healthy, and migrations must complete, before the API starts. auth.localhost is mapped to the Docker host gateway so the API container observes the same issuer as the browser.
docker compose --profile application up --build -d
docker compose --profile application run --rm migrate pnpm seed
docker compose psInspect API logs with docker compose logs -f api. Stop containers with docker compose --profile application down. Named database/Redis volumes are retained. docker compose down -v permanently deletes local container data and should be used only when that is explicitly intended.
In a real deployment, execute the migration image as a controlled release job before rolling out the runtime image. Do not run migrations independently from every API replica.
The application validates environment variables during bootstrap. .env.example is safe local documentation, not a production secret file.
| Variable | Local default/example | Purpose |
|---|---|---|
NODE_ENV, PORT |
development, 3000 |
runtime mode and HTTP listen port |
DATABASE_URL |
local Compose PostgreSQL URL | marketplace Prisma connection |
REDIS_HOST, REDIS_PORT, REDIS_PASSWORD |
localhost, 6379, empty |
BullMQ connection |
ENABLE_QUEUES |
true |
durable Redis jobs; false is test/development-only and production validation rejects it |
ZITADEL_ISSUER |
http://auth.localhost:8080 |
exact public issuer used for discovery and management APIs |
ZITADEL_PROJECT_ID |
Terraform output | required API audience and project role-claim key |
ZITADEL_WEB_CLIENT_ID |
Terraform output | public Authorization Code + PKCE browser client |
ZITADEL_API_CLIENT_ID |
Terraform output | private-key JWT client for introspection |
ZITADEL_API_PRIVATE_KEY_PATH |
ignored JSON file | API application key; required in production |
ZITADEL_PROVISIONER_KEY_PATH |
ignored JSON file | machine key for vendor/staff IAM workflows |
ZITADEL_PLATFORM_ORG_ID |
Terraform output | only organization trusted for platform capabilities |
ZITADEL_CUSTOMER_ORG_ID |
Terraform output | B2C identity and customer-assignment organization |
ZITADEL_WEB_SCOPES |
derived | refresh, API-audience, and organization-role browser scopes |
ZITADEL_PROVISIONING_ENABLED |
false |
enables ZITADEL management mutations after setup is verified |
CORS_ORIGINS |
http://localhost:3001 |
exact origins; HTTPS-only in production, bearer CORS only |
WEBHOOK_MOCK_SECRET |
local placeholder | local provider webhook HMAC secret |
DEFAULT_CURRENCY, DEFAULT_COMMISSION_BPS |
INR, 1000 |
default money/commission policy |
Production bootstrap requires HTTPS origins, both ZITADEL key paths, and the refresh/audience/role web scopes, and rejects the built-in webhook secret. Supply all secrets through the deployment platform; do not commit root/ZITADEL .env, private-key JSON, Terraform state, or migration exports.
| Command | Purpose |
|---|---|
pnpm start:dev |
watch-mode API |
pnpm build |
compile production output |
pnpm start:prod |
run compiled dist/main |
pnpm format / pnpm format:check |
write/check Prettier formatting |
pnpm lint / pnpm lint:fix |
check/fix ESLint issues |
pnpm typecheck |
strict TypeScript check without emit |
pnpm test |
unit/integration suite |
pnpm test:e2e |
end-to-end HTTP suite |
pnpm test:cov |
test coverage report |
pnpm prisma:generate |
generate Prisma Client |
pnpm prisma:validate |
validate Prisma schema |
pnpm migrate:dev -- --name name |
author and apply a development migration |
pnpm migrate:deploy |
apply pending checked-in migrations |
pnpm migrate:status |
inspect migration state |
pnpm seed |
load fake development marketplace fixtures |
pnpm zitadel:migration:export |
read-only legacy IAM migration dry run |
The seed no longer creates credentials, sessions, roles, or permissions. It creates local commerce rows linked to synthetic ZITADEL subjects such as seed-customer and seed-super-admin, plus two provisioned vendor fixtures, catalog/inventory, a cart/address, media references, commission policy, and audit data.
Synthetic subjects are deterministic test cross-references, not login accounts. For interactive local use, create users in ZITADEL and assign the appropriate manifest bundle under the customer, platform, or vendor organization; the API then creates the local User on the first valid request. Existing installations use the migration procedure in docs/zitadel.md instead of recreating users.
Swagger shows the available routes, request DTOs, and authentication requirements; operation-specific permissions are enforced by guards and ownership policies. A representative local happy path is:
- Sign in through ZITADEL Hosted Login, send the access token to the API, create a vendor application, and submit verification metadata.
- Use a ZITADEL platform-admin identity to invoke the explicit vendor review/approval action; ZITADEL vendor organization provisioning must complete before activation.
- As that vendor, create a draft product, variants/media references and inventory, then submit it.
- As an administrator/moderator, approve/publish the product.
- As the customer, browse the public product collection and add variants from one or more vendors to the cart.
- Call checkout with an
Idempotency-Key. The server reprices the cart, quotes shipping, stores the checkout snapshot, reserves inventory, and creates a mock payment. - In development, call
POST /api/v1/payments/{paymentId}/mock-captureas the owning customer. It creates a signed mock event and passes it through the real webhook verifier. Authoritative capture atomically commits reservations and creates the marketplace/vendor-order item snapshots; call it again to observe idempotent handling. - As each vendor, accept/process/mark ready only its own vendor order, then create it with
POST /api/v1/shipments/vendor-orders/{vendorOrderId}and a newIdempotency-Key. - In development, call
POST /api/v1/shipments/{shipmentId}/mock-advanceas that vendor forPICKED_UP,IN_TRANSIT,OUT_FOR_DELIVERY, andDELIVERED; inspect the customer order, ledger/commission, and settlement eligibility. - As the purchasing customer, create a verified-purchase review. The basic return/refund path can be tested from the same delivered order.
Never simulate payment by changing an order/payment status in the database or by trusting a client-side success value. The provider webhook/verification path is part of the workflow.
The repository ships functional deterministic/in-memory mock adapters for payments, shipping, settlement, KYC, media, search and support, plus logging email/SMS adapters. Public catalog discovery itself queries persistent published products in PostgreSQL with basic keyword/filter/sort behavior. The contracts are designed for replacement, but no Razorpay, Cashfree, Shiprocket, Resend, MSG91, Cloudinary, or external helpdesk API is enabled by default.
Mock provider HMACs use WEBHOOK_MOCK_SECRET. Provider state held by an in-memory adapter resets when the API process restarts; PostgreSQL domain records and the public catalog remain authoritative. See Integrations and background jobs before adding a real adapter.
- All REST resources are under
/api/v1; Swagger is intentionally outside the prefix. - Protected routes use
Authorization: Bearer <ZITADEL access token>; ID tokens are not API credentials. - Critical retryable mutations use
Idempotency-Key. - Success is
{ "data": ..., "meta": ..., "requestId": "..." };metais included when the operation supplies it. - Errors are
{ "error": { "code": "...", "message": "...", "details": ... }, "requestId": "..." }. - Persisted
BigIntmoney fields are returned as base-10 JSON strings; currency is explicit and calculations use integer minor units. - Prices/totals, permissions, resource ownership, payment state, and order state are recalculated or verified server-side.
For the full local verification path, ensure the Compose infrastructure is healthy and .env points to the development database, then run:
pnpm prisma:validate
pnpm migrate:deploy
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm test:e2e
pnpm buildThe GitHub Actions workflow provisions isolated PostgreSQL and Redis service containers, installs from the pnpm lockfile, generates/validates Prisma, deploys migrations, seeds end-to-end fixtures, checks formatting/lint/types, runs unit and database-backed end-to-end tests, reruns the seed to verify repeat safety, and builds.
The automated service suite covers ZITADEL claim validation, capability mapping and cross-vendor isolation, serializable conditional inventory reservation, cart/checkout currency enforcement, checkout idempotency and expiry conflicts, promotion finalization, domain state machines, vendor-order splitting, delivery convergence, money/commission/refund allocation, settlement rebuilding, mock provider HMAC/idempotency, BullMQ disabled mode, and return/review/dispute eligibility. The opt-in PostgreSQL e2e suite overrides introspection with deterministic test tokens while exercising the real global guard, local identity linking, seller registration through product publication, a simultaneous two-customer checkout race, and a two-vendor cart through delivery, ledger/commission, settlement eligibility, and verified review.
CI enables that database-backed flow automatically. To run it locally against a disposable migrated/seeded database:
# macOS/Linux
RUN_DATABASE_E2E=true pnpm test:e2e
# PowerShell
$env:RUN_DATABASE_E2E='true'; pnpm test:e2eRun the database-backed tests only against a disposable development/test database: they intentionally create seller/catalog records and orders and commit inventory. The repeat-safe seed does not overwrite changed stock or remove historical commerce records.
- Back up PostgreSQL and test restore procedures before production. Redis persistence does not replace database backups.
- Back up ZITADEL PostgreSQL independently and protect/test recovery of its immutable master key.
- Monitor readiness, structured errors, webhook failures, exhausted BullMQ jobs, reconciliation mismatches, inventory conflicts and settlement failures.
- Scale API replicas only after ensuring every critical mutation and job handler remains idempotent.
- Keep all provider credentials in a secret manager and redact them from logs/job payloads.
- Review migration SQL and run it as a controlled release step.
- Reconcile ZITADEL organizations/grants/assignments with vendor provisioning operations and audit role assignment, moderation, refunds, ledger adjustments, settlements, and disputes.
- Payment, payout, shipping, email, SMS, KYC, media, search and support adapters are local mocks/loggers; production provider certification, sandbox fixtures and automated provider/bank reconciliation ingestion are not included.
- Mock provider state is process-local. Public search persists in PostgreSQL but is limited to basic
containsfiltering and in-process price ordering; indexed full-text/faceted search is required at larger scale. - Tax, promotion funding, shipping-rate policy, fraud, recommendation, sophisticated KYC automation and helpdesk workflows are intentionally constrained extension points.
- Settlement eligibility is an MVP policy, not jurisdiction-specific escrow/payout compliance. Obtain financial/legal review before moving real marketplace funds.
- The backend does not include customer, vendor or admin frontends. A client must implement Hosted Login Authorization Code with PKCE and use
/api/v1/auth/config; the API intentionally returns no auth UI. - The self-hosted ZITADEL Compose assets are a pinned local/semi-production baseline, not a highly available production deployment. SMTP, trusted TLS/FQDN, backups, monitoring, key custody, and upgrade rehearsals are operator responsibilities.
- BullMQ workers run in each API process when enabled; an independently scalable worker entry point and queue administration UI are not included.
- Return approval and refund accounting work, while reverse pickup/tracking/receipt/inspection commands are not yet wired to the shipping provider's return-shipment port.
- Swagger documents authentication, DTO inputs, operation summaries, standard response envelopes and major errors; many domain-specific success payloads still use a generic
dataschema rather than dedicated response DTOs. - Load/performance, penetration, disaster-recovery, provider-contract and production migration rehearsal remain pre-launch work.
- Database-backed HTTP tests cover seller application/verification, vendor approval, product composition/moderation/publication, simultaneous limited-stock checkout contention, and the seeded cart-to-delivery/settlement/review path. Return/refund/dispute workflows remain service-tested rather than one continuous HTTP scenario.
- Add sandbox-backed payment/payout and shipping adapters with provider contract tests, webhook replay protection and reconciliation reporting.
- Persist/rebuild search in PostgreSQL full-text or a selected index and replace mock media with signed object-storage uploads.
- Add a real self-hosted ZITADEL integration-test job covering Hosted Login, introspection, customer default assignment, vendor provisioning, revocation, and migration reconciliation.
- Add metrics/tracing/error reporting, queue dashboards/alerts, backup automation and production runbooks.
- Complete jurisdiction-specific tax, KYC, payout, retention and marketplace compliance review.