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.
Monorepo: API gateway, scheduled workers, admin console, integration reference app, and ten shared packages
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.
The operator's own server. It owns their users and brand; it rents the trading engine.
The core lifecycle tenants integrate against.
Highlights
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
7Tenancy & access
5Cross-cutting
7Shared packages
10Stack by layer
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.