All Projects

Live

eCommerce Platform / Marketplace API

VAAD Media — eCommerce & Advertising Platform

A full-stack REST API for an outdoor and print advertising marketplace where clients can browse, book, and pay for billboard and print media ad spaces across Nigeria.

Role: Backend Lead / Sole Backend Developer


Node.jsTypeScriptMongoDBPaystackAWS S3Express.jseCommerceSharp
Problem Statement

Advertisers in Nigeria have no centralized platform to discover, compare, and book outdoor advertising spaces (static billboards, LED billboards, BRT buses, lampposts) and print media. The traditional process required cold calls, manual invoicing, and uncertain availability checks — all of which VAAD digitizes and automates.

Target users: Marketing managers, advertising agencies, and business owners in Nigeria looking to book outdoor ad spaces or print media products, plus VAAD platform admins who manage listings, invoices, and fulfillment.

Project Walkthrough

VAAD started with a clear market gap: there was no centralized platform in Nigeria where advertisers could discover, compare, and book outdoor advertising spaces — billboards, LED displays, BRT buses, and lampposts — without going through manual phone calls and paper invoices. I was brought in to design and build the complete backend for this advertising marketplace from the ground up, covering everything from user authentication through payment processing and order fulfillment.

The core data modeling challenge was designing a single order system that could cleanly handle two fundamentally different product types. Billboard orders involve duration windows (start date, end date, total days), geographic routes, and availability tracking; print orders involve dimensions, finishing options, delivery methods, and design file uploads. Rather than creating separate order flows for each, I used a discriminated orderItem array within a single Order document — each item carries an orderType field that gates which fields are required and how fulfillment progresses.

The checkout flow was designed to accommodate two real-world payment behaviors. Some advertisers want to pay immediately and get a Paystack redirect link in the same API response. Others prefer to reserve an ad space and handle payment within a window. On the Paystack side, I implemented HMAC-SHA512 webhook signature verification using Node's native crypto module, and added idempotency protection by checking whether a transaction record for that Paystack reference already exists before processing.

One aspect I'm particularly pleased with is the image pipeline for listing management. Rather than storing raw uploads, every image goes through Sharp server-side before reaching S3 — resized to a max of 1000x1000 pixels, which cuts storage and CDN costs without any quality loss for the display sizes used on the frontend.


Tech Stack

Languages

TypeScript

Frameworks

Express.js v5 (beta)Zod (schema validation)

Databases

MongoDB (Mongoose v7 ODM)

Cloud & Infrastructure

www.vaad.com.ng (production)Vercel (frontend)AWS S3 (media image storage)Docker

Dev Tools

Docker + docker-compose.ymlNodemonESLintMorgan (HTTP request logging)Multer (multipart file upload handling)Sharp (server-side image resizing to 1000x1000 before S3 upload)Agenda (MongoDB-backed job scheduler)

Authentication

JWT (access token 24h for users, 1h for admins) + refresh token stored on User/Admin documents + bcrypt 10 rounds + email verification via UUID token with 10-min expiry + OTP-based password reset + Google OAuth 2.0

Third-Party APIs & Integrations

Paystack — order and invoice payment initiation + HMAC-SHA512 signed webhook verification for charge.success events

Mailgun — transactional email (10+ email templates)

Google OAuth 2.0 — Google sign-in/sign-up for users

AWS S3 — media image storage via @aws-sdk/client-s3


System Architecture

Architecture Pattern

Layered MVC monolith. Routes (thin HTTP binding) -> controllers (business logic + orchestration) -> services (shared infrastructure: email, file upload, payment) -> models (Mongoose schemas).

Request Data Flow

Client -> HTTPS -> Express.js -> CORS whitelist check -> express.json() -> Morgan logging -> /api/v1 router -> route file -> auth() middleware (JWT verification + RBAC check) -> Controller method -> Mongoose model query / Paystack axios call / Mailgun email send -> res.ok() / res.created() / res.error() response utility

Key Engineering Decisions

1. Express v5 (beta): native async/await error forwarding. 2. MongoDB + Mongoose: flexible schema design suited to two fundamentally different media types sharing one order document via a discriminated orderItem array. 3. Custom responseUtilities middleware: augments res with res.ok(), res.created(), res.error(), res.noContent() for consistent JSON envelopes. 4. Server-side image compression with Sharp: images resized to max 1000x1000 before S3 upload. 5. Dual checkout flow (Pay Now vs. Pay Later): Pay Later creates the order at Pending status; Pay Now creates the order and simultaneously generates a Paystack authorization_url.

Database Design

Key collections: User (userCustomId unique, authType {password/googleUuid}, emailConfirmation subdocument with token+expiry, passwordRecovery OTP subdocument, refreshToken field), Admin (adminType enum [Super-Admin/Sub-Admin]), billboardMediaApplication (mediaType enum [Static Billboard/Led Billboard/BRT Bus/Lampost], status enum [Available/Unavailable], pictures array, favoriteCount), Order (orderCustomId, amount {subTotal, vat, delivery, totalAmount}, paymentStatus enum [Pending/Failed/Success], paymentType enum [Pay Now/Pay Later]), Transaction (transactionCustomId = Paystack reference, card brand/channel/last4), Invoice (adminCustomId, dueDate, paymentStatus).

Module Structure

authusersadminsmediaApplications (billboard)printMediaApplicationordersinvoicewebhookstransactionsnotificationsfavorites

Key Features & Implementation

Dual Checkout Flow (Pay Now / Pay Later with Paystack)

Users can place an order and pay immediately via Paystack redirect, or defer payment. A separate endpoint regenerates payment links for unpaid orders.

How it was built

Both flows create an Order document first. payNow additionally calls PaystackService.payWithPaystack() returning authorization_url. Amount validation: server re-calculates subtotal from orderItems and validates totalAmount = subTotal + VAT + delivery — rejects on mismatch.

Billboard Media Listings with Full-Text Search

Admins create and manage outdoor ad listings across 4 media types with rich metadata and photos. Users browse, filter, and search listings.

How it was built

MongoDB compound wildcard text index ($**) enables full-text search across all string fields. $text: {$search: keyword} with textScore ranking. Filter by mediaType/state/cityLga/price/availability via dynamic query construction.

Media Image Management (S3 + Sharp)

Admins upload multiple images per listing. Images are server-side compressed and stored in AWS S3 by folder. Individual images can be deleted without removing the listing.

How it was built

Multer receives multipart uploads to local disk. Sharp resizes to max 1000x1000px. PutObjectCommand uploads to S3 with folder-namespaced UUID key. Delete: URL parsed to extract S3 object key, DeleteObjectCommand issued. Temp local files deleted after S3 upload.

Admin Invoice System

Admins generate invoices for offline clients. Invoice is automatically linked to a Paystack payment link and emailed to the customer.

How it was built

Server calculates: subtotal = unitPrice * quantity * period; taxAmount = subtotal * (tax/100); total = subtotal + taxAmount. Paystack payment link generated with paymentType: Invoice in metadata. Webhook differentiates Invoice from Order payments via metadata.paymentType.


Challenges & Engineering Decisions
1

Heterogeneous order items: billboard and print orders have fundamentally different data shapes stored in a single orderItem array via a discriminated orderType field — requires type-checking at every access point.

2

Client-provided unit prices: the server validates that subtotals match client-sent values but trusts client-provided unit prices. Ideally, prices would be fetched from DB server-side.

3

Google OAuth authorization code flow: requires careful redirect URI coordination between frontend and backend.

4

Refresh token stored in DB: simple but means every refresh and logout requires a DB write.


API Documentation

API Route Namespaces

/api/v1/auth/api/v1/users/api/v1/admins/api/v1/mediaApplications/api/v1/printMediaApplication/api/v1/orders/api/v1/invoice/api/v1/webhooks/api/v1/transactions/api/v1/reports/api/v1/notifications/api/v1/favorites