All Projects

Live

SaaS API Platform

Favvii — Multi-Vendor Marketplace

A production-grade NestJS backend powering a full multi-vendor marketplace — from product listings and custom bid requests, to dual payment gateway processing and automated vendor payouts.

Role: Backend Lead / Sole Backend Developer


NestJSTypeScriptMySQLRedisPaystackStripeSocket.IOBull QueuesMulti-VendorRSA JWT
Problem Statement

Building a marketplace where multiple independent vendors can list products, receive custom service requests, communicate with buyers, process payments, and get automatically paid — while giving platform administrators full control over KYC verification, commissions, disputes, and system configuration — is architecturally complex. Off-the-shelf solutions do not handle the custom request/bid flow, split payments, or the deferred vendor payout model required for buyer protection.

Target users: Three distinct user tiers: (1) Buyers — consumers browsing products, placing orders, and submitting custom service requests; (2) Vendors — independent sellers managing storefronts, product catalogs, bids, and payouts; (3) Admins — platform operators managing KYC verification, commission rates, disputes, banners, and system health.

Project Walkthrough

Favvii started from a clear architectural question: how do you build a marketplace that simultaneously serves buyers browsing products, independent vendors running storefronts, and platform administrators governing the entire ecosystem — each with distinct permissions, workflows, and real-time needs? I designed and built the entire backend as a domain-driven monolith on NestJS v10, deliberately choosing that pattern over microservices to keep deployment simple while still enforcing clean domain boundaries. Each of the 24+ domain modules owns its controller, service, and TypeORM entities. Cross-domain coordination happens through typed Bull job queues and an internal event emitter rather than direct service imports, which means the orders module never directly calls the wallet module — it fires an event, and the wallet module reacts.

Authentication was the foundation everything else stood on, so I invested heavily there. Instead of symmetric HS256 JWTs, I used RSA asymmetric key pairs (RS256) — the private key signs tokens, the public key verifies them, enabling future services to validate tokens without sharing secrets. Rather than a global expiry, each role gets a different lifetime: admin sessions expire in 1 hour as a security measure, vendor sessions in 24 hours, and user sessions in 30 days. I also implemented a session handle pattern — login stores a random 32-byte hex token alongside the JWT, and JwtAuthGuard checks the database that this token is still non-null. Logout nulls it out, giving us instant session invalidation without a token blacklist.

The payment and payout system was the most complex piece to get right. I integrated both Paystack and Stripe behind a unified abstraction — both gateways initialize a checkout session on order creation and confirm payment asynchronously via signed webhooks. The vendor payout model adds another layer: earnings go into a 'pending' balance after payment confirmation, with a 10% platform commission deducted and a 7-day post-delivery hold before funds become available. A nightly cron job at 2 AM scans for eligible pending transactions and moves them to 'available'. The commission rate, hold period, and withdrawal limits are all stored in a database-backed WalletSettings record and are admin-configurable at runtime without any code changes.


Tech Stack

Languages

TypeScript

Frameworks

NestJS v10 (Node.js)

Databases

MySQL (primary relational DB via TypeORM)Redis (Bull queue backend + WebSocket session store)SQLite (E2E test database)

Cloud & Infrastructure

AWS S3 (media/document storage)Vercel (intermediate Google OAuth redirect HTML page)

Dev Tools

Swagger / OpenAPI (@nestjs/swagger)Docker + docker-composePM2 (production process management)Jest (unit + E2E testing)Husky + commitlint (git hooks)GitLab CI

Authentication

JWT RS256 (RSA asymmetric key pairs) with role-differentiated expiry times (Admin: 1h, Vendor: 24h, User: 30d); OTP-based email verification; Google OAuth2 (authorization code flow); biometric authentication flag; token invalidation on logout via random hex session token nulling

Third-Party APIs & Integrations

Paystack — primary payment gateway (NGN), webhook-driven order confirmation

Stripe — secondary/international payments, webhook event handling

Mailgun — transactional email delivery (mailgun.js)

Google OAuth2 / google-auth-library — social login

Firebase FCM (expo-server-sdk) — mobile push notifications

AWS S3 (@aws-sdk/client-s3) — media file storage (product images, KYC documents)


System Architecture

Architecture Pattern

Domain-Driven Modular Monolith using NestJS module system with a layered Controller → Service → Repository (TypeORM) pattern. Event-driven side effects via @nestjs/event-emitter. Async background processing via Bull + Redis. Real-time communication via Socket.IO WebSocket gateway with Redis-backed socket session storage.

Request Data Flow

Client sends HTTP request to versioned URI (/api/v1/...) → NestJS Controller receives it (DTO deserialized and validated via global ValidationPipe) → JwtAuthGuard validates JWT → RolesGuard checks @Roles() metadata → Controller calls Service method → Service executes business logic against TypeORM repositories → MySQL → Returns data to Controller → HTTP response sent. Async side effects are enqueued into Bull job queues processed by @Processor handlers. Real-time events flow through Socket.IO gateway with Redis-backed user→socket mappings.

Key Engineering Decisions

1. RSA asymmetric JWT (RS256) over symmetric HS256 — enables future multi-service token verification. 2. Role-differentiated JWT expiry — admin sessions expire in 1h for security; vendor: 24h; user: 30d. 3. Bull + Redis for async processing — emails, push notifications never block HTTP cycle. 4. Internal event bus for domain decoupling — order module fires an event, wallet module reacts. 5. Deferred vendor payout (pending→available after 7 days post-delivery) as buyer protection, configurable at runtime via admin wallet settings. 6. Dual payment gateway handled by a shared PaymentHelperService abstraction — both require HMAC signature verification before any state mutation.

Database Design

MySQL with TypeORM. Snake_case naming strategy applied globally. Key tables: users, vendor_profiles, products, product_variants, product_inventory, delivery_options, orders, order_items, order_tracking, vendor_wallets, wallet_transactions, wallet_settings, conversations, messages, inapp_notifications, kyc, support_tickets, reviews, transactions.

Module Structure

authusersuser-addressesvendor-profilesvendor-wallets (+ withdrawals sub-module, + bank-accounts sub-module)categoriesproducts (+ product-variants, product-inventory, delivery-options sub-entities)beeds (custom buyer service/product requests)vendor-offers (vendor bids on buyer beeds)shopping-cartsorders (+ order-tracking sub-module)transactionspayment-webhooks (Paystack + Stripe handlers)refundsreviewsconversations (+ WebSocket ChatGateway + Redis socket service)kycdisputesbannersdashboardssupport-ticketssettingscommunications (email + push-notifications + inapp-notifications + sms + communication-logs)fileshealth (@nestjs/terminus health checks)task-scheduler (4 cron jobs)

Key Features & Implementation

Multi-Role Authentication & Authorization

Three user roles (user, vendor, admin) each with distinct capabilities and JWT expiry durations. Registration supports email+password with OTP email verification, Google OAuth2 social login, biometric auth flag, and token-based session invalidation on logout.

How it was built

JWT RS256 with RSA asymmetric keypairs. Token expiry is dynamically selected by role at issue time via getExpirationTimeByRole(). Login stores a random 32-byte hex token in the users table — the token acts as a revocable session handle. JwtAuthGuard validates the JWT signature and confirms the user.token field in the DB is non-null, making logout instant without token blacklists. Google OAuth uses the authorization code flow with a Vercel-hosted intermediate redirect page for mobile deep-link support.

Vendor Wallet & Automated Payout System

Each vendor has a digital wallet. Earnings land in 'pending' after order payment, and automatically move to 'available' after a configurable hold period post-delivery. Vendors can request manual withdrawals from available balance.

How it was built

walletService.creditPending() computes commission (default 10%, taken from WalletSettings in DB), records net_amount, and saves a pending WalletTransaction. A daily cron job (2 AM Africa/Lagos) finds all pending transactions past the delivery cutoff date and calls creditAvailable() to move funds. All balance mutations use TypeORM DataSource transactions for atomicity.

Custom Request (Beed) & Bidding System

Buyers post open requests ('beeds') for custom services or products. Vendors respond with competitive offers including pricing and timelines. The buyer selects a winning offer and places an order directly from it.

How it was built

Beed entity captures buyer requirements. VendorOffer entity captures each vendor's proposal. Order entity has an optional offer_id FK — when a beed-driven order is created, it links to the accepted VendorOffer. Order creation from an offer follows a distinct validation path in OrdersService.create().

Dual Payment Gateway Integration (Paystack & Stripe)

Supports both Paystack (Nigeria/Africa, NGN) and Stripe (international), selectable at order time, with cryptographically signed webhook handling for reliable async payment confirmation.

How it was built

PaymentHelperService abstracts gateway selection. Webhooks arrive at /api/v1/webhooks/payments/paystack and /api/v1/webhooks/payments/stripe. Paystack: HMAC-SHA512 verification. Stripe: stripe-signature header verification. Both verify before any DB mutation. On success: Transaction confirmed, Order payment_status updated, wallet crediting and notifications triggered via events.

Real-Time Messaging with WebSocket & Redis

Buyers and vendors communicate in real-time conversation threads. Messages are delivered instantly via WebSocket with no polling.

How it was built

NestJS WebSocket gateway (ChatGateway) backed by Socket.IO. On connection, the gateway extracts and validates the JWT from socket handshake headers. RedisSocketService persists user-to-socket mappings in Redis, enabling delivery even across multiple server instances.

Multi-Channel Notification System

Users receive notifications via four channels: email, SMS, mobile push, and in-app — with per-user preference flags controlling which channels are active.

How it was built

CommunicationsModule contains four sub-modules: email (Mailgun + Handlebars templates), push-notifications (Firebase FCM via expo-server-sdk), inapp-notifications (persisted to DB + delivered via WebSocket), sms. All notification dispatch is async via Bull queues.


Challenges & Engineering Decisions
1

Atomic Vendor Payouts: The pending→available wallet flow needed strict atomicity. TypeORM DataSource transactions wrap every balance read-modify-write operation, and the cron job processes each eligible transaction individually with per-item error handling.

2

Dual Payment Webhook Normalization: Paystack and Stripe use fundamentally different event payload shapes and signature schemes. Each gateway has its own dedicated controller method and service handler.

3

Google OAuth for Mobile Apps: Native mobile apps cannot receive standard OAuth redirect URLs. The solution: a minimal Vercel-hosted HTML page acts as an intermediate redirect that re-redirects to the mobile custom scheme.

4

WebSocket Auth at Handshake Time: NestJS HTTP guards do not run on WebSocket connections. A custom authentication step runs inside handleConnection().


API Documentation

API Route Namespaces

/api/v1/auth/api/v1/users/api/v1/vendor-profiles/api/v1/vendor-wallets/api/v1/categories/api/v1/products/api/v1/beeds/api/v1/vendor-offers/api/v1/shopping-carts/api/v1/orders/api/v1/transactions/api/v1/webhooks/payments/paystack/api/v1/webhooks/payments/stripe/api/v1/conversations/api/v1/kyc/api/v1/disputes/api/v1/health