Skip to content

RRimeKS/adbidz-case-study

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

Adbidz — B2B Auction-Based Advertising Marketplace

Adbidz Marketplace

Hook

A two-sided B2B marketplace connecting venue owners with advertisers through a real-time auction engine for branded physical consumables (napkins, cups, menus). Shipped solo in 5 months, live across 10+ countries, with event-driven architecture, 5-role RBAC, and a fully automated AWS stack provisioned with Terraform.

🔗 Live Platform · Status: Production (MVP phase)


⚡ Key Achievements

  • 🌍 10+ countries served from day one with country-scoped data isolation
  • ⚔️ Zero race conditions on auction expiry via pessimistic locking + double-check pattern
  • 🔔 Event-driven notifications — email + Socket.io + in-app fan-out from a single event bus
  • 🔐 5-role RBAC (Super Admin, Country Manager, Host, Brand, Supplier) with row-level security
  • 🏗 Full AWS stack in Terraform — VPC, ECS Fargate, RDS, CloudFront, API Gateway, Route 53
  • 🚀 ~3 min deploys via GitHub Actions with zero-downtime rolling updates
  • ⚙️ Runtime-configurable commission engine with 5-min cached settings service
  • 🔑 Auth0 OAuth2/JWT integration with JWKS verification for both REST and Socket.io

👤 My Role

Solo Full-Stack Developer + AWS Solutions Architect (Nov 2025 – Mar 2026)

End-to-end responsibility:

  • Architecture design — every component, service boundary, and data model
  • Backend development — Node.js + Express + TypeScript, Sequelize ORM, Socket.io server
  • Frontend development — React 19 + TypeScript, Zustand state, Radix UI, i18n
  • Infrastructure — entire AWS stack defined in Terraform (/iac)
  • DevOps — GitHub Actions CI/CD pipelines, ECR + ECS Fargate deploys
  • Third-party integrations — Auth0 identity, Resend email, Google Places API
  • Product decisions — scope negotiation, MVP prioritization with client product owner

Only external dependency: the client product owner for domain decisions and QA.


🎯 The Problem

Advertisers struggled to reach niche, offline audiences in specific venues (restaurants, cafés, event halls). Venue owners had idle surface real estate (napkins, cups, menus) but no way to monetize it efficiently.

Existing solutions were broken:

  • Direct deals between venues and brands required manual negotiation
  • No transparent pricing → underpriced deals for venues, overpriced for brands
  • No standardized supply chain for branded consumables
  • Geographic fragmentation — brands couldn't scale across countries
  • No centralized platform to manage campaigns, orders, and fulfillment

Goal: Build an auction-based marketplace where brands competitively bid on venue "slots", with integrated supply chain for manufacturing and delivery of branded consumables.


💡 The Solution

An end-to-end two-sided marketplace with:

  • Real-time auction engine — brands bid on listings, highest bidder wins when auction expires
  • Buy-now option — instant purchase at listed price without waiting for auction
  • Multi-role platform — Super Admin, Country Manager, Host, Brand, Supplier — each with scoped permissions
  • Country-scoped operations — managers oversee specific regions, data isolated per country
  • Integrated supply chain — supplier dashboard for order fulfillment
  • Configurable commission engine — runtime-adjustable platform fees (default 15%)
  • Real-time notifications — Socket.io push notifications for bids, orders, campaign updates
  • Multi-language (EN/TR) — i18next with lazy-loaded translations
  • Google Places integration — geolocation and region validation for venue listings
  • Campaign management — brands track performance, venues track earnings

🏗 Architecture

High-level flow:

                      CloudFront CDN
                           │
           ┌───────────────┼───────────────┐
           ▼                               ▼
    ┌──────────────┐                ┌─────────────────┐
    │ S3 (Vite SPA)│                │   API Gateway   │
    │  React 19    │                │  (Regional)     │
    └──────────────┘                └────────┬────────┘
                                             │
                                             ▼
                              ┌──────────────────────────┐
                              │   ALB (Load Balancer)    │
                              └────────────┬─────────────┘
                                           │
                              ┌────────────▼─────────────┐
                              │     ECS Fargate Tasks     │
                              │   Node.js + Express       │
                              │   + Socket.io Server      │
                              └──┬────────────────────┬──┘
                                 │                    │
                                 ▼                    ▼
                          ┌─────────────┐     ┌──────────────┐
                          │   RDS       │     │      S3      │
                          │ MySQL       │     │ (Uploads)    │
                          └─────────────┘     └──────────────┘

         ┌─────────────────────────────────────────────┐
         │  Auth0 (OAuth2 / JWT via JWKS)              │
         │  AWS Systems Manager Parameter Store        │
         │  CloudWatch (Logs + Metrics)                │
         └─────────────────────────────────────────────┘

Request lifecycle:

  1. Browser → CloudFront (CDN) → S3 (static React SPA)
  2. API calls from SPA → CloudFront → API Gateway → ALB → ECS Fargate task
  3. Socket.io WebSocket → ALB (sticky session) → ECS Fargate (same task for connection lifetime)
  4. Backend → RDS MySQL (transactions) + S3 (presigned upload URLs) + Auth0 (JWKS validation)
  5. CloudWatch collects logs/metrics; Systems Manager serves secrets to ECS tasks

🧠 Tech Decisions (Why, not What)

Why ECS Fargate over EC2?

Decision: Serverless container orchestration. Reason: Zero server patching, automatic scaling based on CPU/memory, pay-per-task pricing. EC2 would have required managing AMIs, SSH access, security updates — unnecessary overhead for a solo developer. Trade-off: Slightly higher per-hour cost than EC2 reserved instances, but saved ~20 hours/month of operational work.

Why API Gateway in front of ECS?

Decision: Regional REST API Gateway fronts the ALB. Reason: Adds a layer for request validation, rate limiting at edge, and future authorizer Lambdas. Decouples clients from backend — if we later split the monolith into microservices, clients don't know. Trade-off: Extra latency hop (~10ms), but buys architectural flexibility.

Why Auth0 instead of rolling our own auth?

Decision: Delegate identity to Auth0. Reason: OAuth2/OIDC is easy to get wrong. MFA, password reset flows, social login, JWKS rotation, token introspection — all handled by a battle-tested provider. Backend just verifies JWTs via JWKS. Trade-off: Auth0 free tier has user limits; upgrade needed at scale. But the time saved on auth UX is worth it.

Why Socket.io + event emitter over polling or direct push?

Decision: Custom appEvents EventEmitter fans out to Socket.io, email, and in-app notifications. Reason: Controllers emit domain events (bid:placed, auction:won) and don't know about downstream consumers. Adding SMS or push notifications later = one new listener, zero controller changes. Trade-off: In-process event bus doesn't scale across tasks. Moving to Redis Pub/Sub would be the next step if we horizontally scale beyond one Fargate task.

Why pessimistic locking for auction expiry?

Decision: LOCK.UPDATE on listing rows during auction expiry, plus double-check after acquiring the lock. Reason: At expiry time, multiple parallel processes could try to declare a winner. Pessimistic locking serializes them; double-check catches the race where another process completed processing between SELECT and LOCK. This guarantees at most one winner and exactly one commission charge. Trade-off: Slower than optimistic locking under contention, but simpler reasoning and correctness-first trade-off for financial transactions.

Why Terraform (not CDK, Pulumi, or CloudFormation)?

Decision: Terraform HCL, modular files per AWS service (vpc.tf, ecs.tf, rds.tf...). Reason: Largest ecosystem, multi-cloud ready, declarative state. CDK was tempting but adds a TypeScript build step; CloudFormation is too verbose; Pulumi is promising but smaller community. Trade-off: HCL has quirks (dynamic blocks, for_each gotchas), but remote state in S3 + DynamoDB lock handles multi-env cleanly.

Why MySQL instead of PostgreSQL?

Decision: RDS MySQL 8.0. Reason: Client's operations team already had MySQL expertise; Sequelize was well-tested on MySQL. PostgreSQL would have given us better JSON handling but didn't justify the learning curve for this team. Trade-off: Slightly weaker JSON querying, but covered by structured tables.


🛠 Tech Stack

Frontend

  • React 19 + TypeScript + Vite
  • Tailwind CSS 4 + Radix UI primitives
  • Zustand for global state
  • Socket.io client for real-time updates
  • i18next — EN/TR localization
  • Auth0 React SDK for authentication
  • Google Places API for venue geolocation

Backend

  • Node.js + Express 5 + TypeScript
  • Sequelize 6 ORM (paranoid mode with soft deletes, UUID primary keys)
  • Socket.io server with Auth0 JWT validation (JWKS via jose)
  • Zod for request validation
  • Resend for transactional email (with HTML templates)
  • Morgan request logging with correlation IDs
  • Helmet + rate limiting + CORS hardening

Infrastructure (AWS)

Layer Service
Compute ECS Fargate + ALB
Container Registry ECR
Database RDS MySQL (Multi-AZ)
Storage S3 (uploads + frontend)
CDN CloudFront
API Gateway Regional REST API
DNS Route 53
Secrets Systems Manager Parameter Store
Monitoring CloudWatch Logs + Metrics
Identity Auth0 (external)

DevOps

  • Terraform IaC (entire stack in /iac)
  • GitHub Actions CI/CD (backend Docker build → ECR → ECS service update, frontend → S3 → CloudFront invalidation)
  • Docker multi-stage builds for minimal image size
  • Automated deployment on push to main

🔍 Deep Dive: Key Technical Achievements

1. Real-time Auction Engine with Concurrency Safety

Auction expiry processor runs every 60 seconds via a scheduler. Each expiring listing is processed in its own database transaction with:

  • Pessimistic locking (LOCK.UPDATE) on the listing row to prevent concurrent winner selection
  • Double-check pattern — re-verify auction status after acquiring the lock to handle race conditions
  • Commission calculation pulled from cached SettingsService (5-min TTL)
  • Event emission on winner declaration triggers email + Socket.io notifications in parallel

This design survives simultaneous bids arriving milliseconds before expiry, ensuring at most one winner per auction and exactly one commission charge.

2. Event-Driven Notification System

A custom event emitter (appEvents) decouples business logic from notification delivery:

  • Controllers emit domain events (bid:placed, order:created, auction:won)
  • Notification handlers listen and fan out to email + Socket.io + in-app notifications
  • Socket.io connections authenticated via Auth0 JWT (JWKS verification)
  • Users join personal rooms (user:{userId}) for targeted delivery
  • No tight coupling — new notification channels added without touching controllers

3. Multi-Role RBAC with Country Scoping

Role Scope
SUPER_ADMIN Global access, all countries
COUNTRY_MANAGER Single country, manages Hosts + Brands
HOST Their own venues + listings
BRAND Their own bids + campaigns
SUPPLIER_USER Their own fulfillment orders

Implemented via:

  • authenticate middleware composition (checkJwt + attachUser + requireUser)
  • authorizeRoles([...]) middleware for route-level checks
  • Country filtering middleware automatically scopes queries for Country Managers
  • Sequelize scopes enforce row-level security at ORM layer

4. Configurable Platform Settings Engine

Runtime-adjustable configuration without deploys:

  • PlatformSetting model — key-value store with typed values (STRING / NUMBER / BOOLEAN / JSON)
  • SettingsService — singleton with 5-minute in-memory cache, refreshes from DB on TTL expiry
  • Admin UI for editing commission rate, maintenance mode, min bid amount, etc.
  • Maintenance mode middleware — returns 503 when enabled, bypasses /health and admin routes

5. Production-Grade Infrastructure as Code

Entire AWS stack defined in Terraform (/iac):

  • Modular structure: vpc.tf, ecs.tf, rds.tf, s3.tf, cloudfront.tf, apigateway.tf, dns.tf, monitoring.tf, cicd.tf, secrets.tf
  • Separate state files for prod/staging (remote backend in S3 + DynamoDB lock)
  • Secrets managed via AWS Systems Manager Parameter Store (referenced in ECS task definitions)
  • Auto-scaling policies on ECS service based on CPU/memory metrics
  • Disaster recovery — full stack recreatable in ~20 minutes with terraform apply

6. Inline Runtime Migrations

In addition to Sequelize CLI migrations, a runtime migration system handles schema changes at boot:

  • runPendingMigrations() in index.ts executes idempotent DDL before sequelize.sync()
  • Uses columnExists() / indexExists() helpers for safe conditional changes
  • Handles indexes, constraints, and migrations that sync() can't express
  • Zero-downtime schema evolution during rolling deploys

📸 Screenshots

Marketplace — Browse and Bid

Marketplace

Listing Detail — Auction + Buy Now

Listing Detail

Admin Dashboard — Platform Operations

Admin Dashboard

Admin Analytics — Business Metrics

Admin Analytics


🔒 Security Highlights

  • Auth0 OAuth2/JWT — battle-tested identity provider, handles MFA and password policies
  • JWKS-based token verification — public key rotation without backend restarts
  • HttpOnly session cookies — XSS-safe
  • Helmet security headers — CSP, HSTS, X-Frame-Options
  • Layered rate limiting — general (500/15min), auth (20/15min), mutation (60/min), sensitive actions (20/min)
  • Body size limit — 10kb to prevent payload attacks
  • AWS Parameter Store for secrets — no secrets in env files or code
  • S3 block-public-access on upload buckets
  • VPC with private subnets — RDS and ECS tasks not exposed to public internet
  • Presigned URLs for secure direct uploads (15 min expiry)
  • Paranoid mode (soft deletes) — no accidental data loss, full audit history

🎓 Key Learnings

Technical:

  • Race condition elimination in auction systems requires both optimistic AND pessimistic tactics
  • Socket.io + ALB needs sticky sessions for WebSocket upgrade; regional API Gateway for fanout
  • ECS Fargate cold starts are minimal but requires careful task definition sizing
  • Terraform modular state is critical when infra grows beyond ~10 resources
  • Event-driven architecture pays off when 3rd notification channel added in 10 minutes

Product:

  • MVP scope creep was the biggest risk — shipped auction + buy-now as v1, deferred subscriptions
  • Country-scoped data is easier to design upfront than bolt on later
  • Admin tooling (settings, maintenance mode) was more valuable than expected for ops team

🚫 What I Can't Share

  • Source code (NDA with client)
  • Internal business logic specifics (commission tiers, supplier pricing)
  • User data, campaign data, financial data

For a public repository demonstrating similar patterns (real-time WebSocket, JWT auth with HttpOnly cookies, RTK Query cache injection), see my FastChat project.

About

Two-sided B2B marketplace with real-time auction engine, 5-role RBAC, multi-country ops (10+ countries). Node.js · TypeScript · Socket.io · React 19 · MySQL · Auth0 · AWS ECS Fargate · Terraform.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors