SaaS / MarketplaceArchitecture record

Multi-Tenant P2P Trading Marketplace

The product is not a marketplace — it is the engine several marketplaces run on. Each tenant operator keeps their own frontend, their own users and their own brand, and integrates the offer, trade and dispute lifecycle over HTTP. That inverts the usual design pressure: almost every hard problem here is about isolation. Tenant data must never leak across a boundary, admin permissions must be clamped to the tenant being acted on, and retried webhooks must not double-execute a trade.

1.4k
TypeScript files
~62k lines
10
Shared packages
in the monorepo
19
Gateway modules
bounded contexts
2
Runtimes
gateway + scheduled workers
Repository shape

Monorepo: API gateway, scheduled workers, admin console, integration reference app, and ten shared packages

How this is presented

Documented as an architecture record. The engine only makes sense with tenants attached, so the artefact is the isolation model and the integration contract.

How a tenant's request stays inside its own lane

The tenant facade is the node to watch. Nothing reaches a business module without passing through it first.

Tenant backendExternal

The operator's own server. It owns their users and brand; it rents the trading engine.

ExternalClient surfaceServiceWorker / jobData storeRealtime
Offer to settled trade

The core lifecycle tenants integrate against.

Highlights

Multi-tenancy enforced at the facade layer, so a tenant's request physically cannot resolve another tenant's data.
Admin scope clamping: an operator's permissions are narrowed to the tenant in context, not merely checked against it.
Idempotency keys on state-changing endpoints, so a retried request settles a trade once rather than twice.
Webhook lifecycle with delivery tracking and replay, treating tenant endpoints as unreliable by default.
Explicit API versioning so tenant integrations do not break when the engine moves forward.
Ten shared packages keep cross-cutting concerns — auth, logging, messaging, config — identical across runtimes.
Polyglot persistence: relational data for the trade ledger, document storage for high-volume unstructured records.

A facade that makes tenancy unavoidable

Every request enters through a tenant facade that resolves who is asking before any business module sees it. Isolation is therefore structural rather than something each module has to remember to enforce.

API gateway

The single HTTP boundary: authentication, tenant resolution, listings, offers, trades, disputes, chat, webhooks and administration.

  • Runs NestJS on Fastify for throughput on a request-heavy workload.
  • Cross-cutting modules for request IDs, rate limiting, error filtering and idempotency.
  • Publishes a versioned OpenAPI surface that tenant backends integrate against.

Tenant facade

Resolves the calling tenant and clamps every downstream query to it.

  • The isolation boundary the rest of the system is built behind.
  • Admin actions are scoped to the tenant in context rather than granted globally.

Worker runtime

Scheduled and asynchronous work: webhook delivery, notifications and periodic reconciliation.

  • Separate deployable so retry storms never consume request capacity.
  • Templated transactional email for tenant and end-user notifications.

Shared packages

Auth, messaging, HTTP client, logging, config, health and test utilities used by every runtime.

  • One implementation of each cross-cutting concern instead of per-app drift.
  • Test utilities shared so both runtimes are exercised the same way.

Reference clients

An internal admin console and a reference integration app showing tenants how to consume the engine.

  • The reference app doubles as living integration documentation.
  • Covers listings, trades, account flows and a webhook inspection log.

Gateway modules and shared packages

Nineteen bounded contexts behind the tenant facade, plus ten shared packages that keep both runtimes consistent. The cross-cutting group is where multi-tenant safety actually lives. 29 units across 4 groups.

Marketplace domain

7
listingsmarketplaceofferstradesdisputeschatsteam

Tenancy & access

5
tenantstenant-facadeauthusersadmin

Cross-cutting

7
idempotencyrate-limitrequest-iderror-filterversioningwebhooksswagger-docs

Shared packages

10
auth-libcommonhttp-clientloggermessagingnest-confignest-healthtest-utilseslint-configtypescript-config

Stack by layer

Clients
Next.js admin consoleNext.js integration reference app
Services
NestJS on FastifyScheduled worker runtimeHealth probes
Data
PostgreSQL via TypeORMMongoDBRedis
Shared packages
Auth libraryMessagingHTTP clientLoggerConfigHealthTest utilities
Integration
OpenAPI / SwaggerSigned webhooksAPI versioningIdempotency keys

Delivered work

Engine and isolation

  • Tenant facade enforcing data isolation structurally rather than per-module.
  • Admin scope clamping so operator permissions narrow to the tenant in context.
  • Nineteen bounded contexts across listings, offers, trades, disputes, chat and administration.
  • Idempotency, rate limiting, request IDs and error filtering as cross-cutting modules.

Integration surface

  • Versioned OpenAPI contract for tenant backends.
  • Signed webhook lifecycle with retries, persistence and an inspectable delivery log.
  • A reference integration app that doubles as living documentation.
  • Internal admin console for operations and dispute review.

Platform foundations

  • Ten shared packages covering auth, messaging, HTTP, logging, config, health and test utilities.
  • Polyglot persistence split between a relational ledger and document storage.
  • Separate worker runtime for scheduled and retrying work.
  • Health probes and structured logging wired through every runtime.

Modules and demo strategy

Tenant facade

Makes cross-tenant leakage structurally difficult.

  • Resolution happens before any business logic runs.
  • The single most important boundary in the system.

Idempotency layer

Ensures a retried request settles a trade exactly once.

  • Keys are held in shared cache so the guarantee survives multiple instances.
  • Essential when the caller is someone else's backend with its own retry policy.

Webhook lifecycle

Delivers events to endpoints that may be down.

  • Persisted delivery state, backoff retries and a tenant-visible log.
  • Decouples the engine's latency from the tenant's uptime.

Dispute workflow

Handles trades that fail to settle cleanly.

  • Freezes automated transitions and routes to human review.
  • Chat history is part of the evidence, not a separate system.

Frontend documentation format

How this project is presented

Isolation model shown as a flow, since that is the defining constraint.
Integration contract described rather than exposed.
Verified module and package counts.
No public instance — the engine has no meaning without tenants.