SewOps — Full System Design

Revenue & Ops
for Nigerian
Tailors.

A WhatsApp-first workflow automation engine. Orders, production, measurements, payments — all triggered from a single chat. MVP designed for <45 day build.

8
Deliverables
6
Core Modules
45
Day Build Plan
<300ms
API Target
System Architecture

Multi-tenant
service topology

CLIENT LAYER
📱 WhatsApp Cloud API
🌐 Next.js Web Dashboard
📲 Mobile Web (PWA)
↓   ↓   ↓
EDGE — Cloudflare
CDN + Static Assets
DDoS Protection
WAF Rules
TLS 1.3
API GATEWAY — NestJS on EC2
WhatsApp Webhook Handler
Auth Service (JWT + Refresh)
Rate Limiter
Request Validator
Tenant Resolver
Guards + Interceptors
CORE SERVICES (Modular NestJS)
Orders Service
Customers & Measurements
Production Pipeline
Payments Service
Automation Engine
AI Parser (Claude API)
Notifications Service
Reports Service
QUEUE LAYER — BullMQ + Redis
whatsapp.outbound queue
automation.triggers queue
payment.reminders queue
ai.parser queue
deadline.alerts queue
DATA LAYER — AWS
PostgreSQL RDS (Primary)
Redis ElastiCache
S3 (Documents + Media)
CloudWatch (Logs)
EXTERNAL INTEGRATIONS
Paystack API
Flutterwave API
WhatsApp Business API (Meta)
Claude AI API
SMTP (Resend)
Multi-tenancy strategy: Each tailor business (tenant) is isolated via tenant_id on every database row. All queries are scoped through a NestJS middleware that resolves tenant from JWT claims. No cross-tenant data leakage is possible at the ORM level via a base repository pattern.
Database Schema

PostgreSQL
entity model

tenants
MULTI-TENANT ROOT
idUUID PK
business_nameVARCHAR(200)
slugVARCHAR(100) UNIQUE
phoneVARCHAR(20)
wa_phone_number_idVARCHAR(50)
wa_access_tokenTEXT ENCRYPTED
planENUM(free,pro,scale)
is_activeBOOLEANDEFAULT true
created_atTIMESTAMPTZ
users
STAFF
idUUID PK
tenant_idUUID FK → tenants
nameVARCHAR(150)
emailVARCHAR(255) UNIQUE
phoneVARCHAR(20)
password_hashTEXT
roleENUM(owner,staff,viewer)
last_login_atTIMESTAMPTZ
created_atTIMESTAMPTZ
customers
PROFILES
idUUID PK
tenant_idUUID FK
nameVARCHAR(150)
phoneVARCHAR(20)
wa_idVARCHAR(30)WhatsApp ID
emailVARCHAR(255)
birthdayDATE
notesTEXT
total_ordersINT DEFAULT 0
total_spentDECIMAL(12,2)
last_order_atTIMESTAMPTZ
created_atTIMESTAMPTZ
measurements
VERSIONED
idUUID PK
customer_idUUID FK
tenant_idUUID FK
versionINT DEFAULT 1
is_currentBOOLEAN DEFAULT true
chestDECIMAL(5,2)inches
waistDECIMAL(5,2)
hipDECIMAL(5,2)
shoulderDECIMAL(5,2)
sleeveDECIMAL(5,2)
inseamDECIMAL(5,2)
neckDECIMAL(5,2)
custom_fieldsJSONBflexible
notesTEXT
taken_atTIMESTAMPTZ
orders
CORE ENTITY
idUUID PK
order_refVARCHAR(20)e.g. SEW-0042
tenant_idUUID FK
customer_idUUID FK
measurement_idUUID FK
assigned_toUUID FK → users
style_nameVARCHAR(200)
style_detailsTEXT
fabric_infoTEXT
reference_imagesTEXT[]S3 URLs
priceDECIMAL(12,2)
deposit_amountDECIMAL(12,2)
balance_dueDECIMAL(12,2)GENERATED
current_stageENUM(received,cutting,sewing,fitting,ready,delivered)
deadlineTIMESTAMPTZ
sourceENUM(whatsapp,dashboard,walk_in)
wa_thread_idVARCHAR(50)
notesTEXT
created_atTIMESTAMPTZ
updated_atTIMESTAMPTZ
order_stages
AUDIT LOG
idUUID PK
order_idUUID FK
tenant_idUUID FK
stageENUM (same as orders)
entered_atTIMESTAMPTZ
exited_atTIMESTAMPTZ
changed_byUUID FK → users
noteTEXT
payments
FINANCIAL
idUUID PK
order_idUUID FK
tenant_idUUID FK
amountDECIMAL(12,2)
currencyVARCHAR(3) DEFAULT 'NGN'
typeENUM(deposit,balance,full)
providerENUM(paystack,flutterwave)
provider_refVARCHAR(100)
payment_linkTEXT
statusENUM(pending,paid,failed,refunded)
link_sent_atTIMESTAMPTZ
paid_atTIMESTAMPTZ
created_atTIMESTAMPTZ
automation_triggers
ENGINE
idUUID PK
tenant_idUUID FK
trigger_typeENUM(customer_inactive,order_ready,unpaid_balance,birthday,upsell,deadline_risk)
is_enabledBOOLEAN DEFAULT true
configJSONBthresholds, delays
message_templateTEXT
last_run_atTIMESTAMPTZ
wa_messages
WHATSAPP LOG
idUUID PK
tenant_idUUID FK
customer_idUUID FK NULLABLE
order_idUUID FK NULLABLE
wa_message_idVARCHAR(100)
directionENUM(inbound,outbound)
from_numberVARCHAR(20)
bodyTEXT
parsed_dataJSONBAI extracted
is_processedBOOLEAN DEFAULT false
received_atTIMESTAMPTZ
Key indexes: tenant_id on all tables (required), customers(wa_id, tenant_id) for webhook lookup, orders(current_stage, deadline, tenant_id) for pipeline queries, payments(status, tenant_id) for revenue reports. All UUIDs use gen_random_uuid().
Backend API

NestJS module
architecture

MODULE 01
AuthModule
JWT auth with refresh tokens. Multi-tenant login. Phone OTP for WhatsApp onboarding. Guards for all protected routes.
POST/auth/register
POST/auth/login
POST/auth/refresh
POST/auth/otp/send
POST/auth/otp/verify
MODULE 02
OrdersModule
Core order CRUD + stage transitions. Auto-assigns order refs. Emits events on stage change for automation engine.
POST/orders
GET/orders
GET/orders/:id
PATCH/orders/:id/stage
PATCH/orders/:id
GET/orders/today
GET/orders/urgent
MODULE 03
CustomersModule
Customer profiles with WhatsApp ID linking. Auto-creates on first WhatsApp message. Tracks order history and LTV.
POST/customers
GET/customers
GET/customers/:id
GET/customers/:id/orders
GET/customers/by-wa/:waId
PATCH/customers/:id
MODULE 04
MeasurementsModule
Versioned measurement records per customer. Immutable history — new version on update. Quick-attach to new orders.
POST/customers/:id/measurements
GET/customers/:id/measurements
GET/customers/:id/measurements/current
POST/customers/:id/measurements/new-version
MODULE 05
PaymentsModule
Paystack + Flutterwave abstracted behind a single PaymentProvider interface. Handles webhooks, reconciliation, and link generation.
POST/payments/generate-link
GET/payments/order/:orderId
POST/payments/webhook/paystack
POST/payments/webhook/flutterwave
POST/payments/send-reminder/:paymentId
GET/payments/unpaid
MODULE 06
WhatsAppModule
Webhook ingestion from Meta. Verifies tokens, parses message types, routes to AI parser, sends outbound messages via queue.
GET/webhook/whatsapp
POST/webhook/whatsapp
POST/whatsapp/send
GET/whatsapp/messages/:customerId
MODULE 07
AutomationModule
Cron-driven trigger engine. Evaluates all tenants' active triggers on schedule. Queues outbound WhatsApp messages via BullMQ.
GET/automation/triggers
POST/automation/triggers
PATCH/automation/triggers/:id
POST/automation/test/:triggerId
MODULE 08
ReportsModule
Revenue summaries, production throughput, customer LTV, unpaid balance reports. Pre-computed via Redis cache for speed.
GET/reports/revenue
GET/reports/pipeline
GET/reports/customers/top
GET/reports/unpaid

Inbound message processing flow

01 — Receive
POST /webhook/whatsapp
Verify X-Hub-Signature-256
Validate token challenge
Extract entry[].changes[]
Return 200 immediately
02 — Parse & Route
Identify message type
(text / image / audio)
Lookup tenant by
phone_number_id
Find/create customer
by wa_id
03 — AI Extract + Act
Queue ai.parser job
Claude extracts: name,
style, deadline, notes
Creates draft order
Sends confirmation WA
whatsapp/whatsapp.controller.ts
TypeScript
@Post('webhook/whatsapp') async handleInbound(@Body() payload: WaWebhookDto, @Headers() headers: Record<string, string>) { // Verify signature immediately — reject fakes this.waService.verifySignature(payload, headers['x-hub-signature-256']); const entry = payload.entry?.[0]?.changes?.[0]?.value; if (!entry?.messages?.length) return { status: 'ok' }; // status update, skip const message = entry.messages[0]; const phoneNumberId = entry.metadata.phone_number_id; // Non-blocking: queue for processing await this.waQueue.add('process-inbound', { phoneNumberId, waId: message.from, messageId: message.id, body: message.text?.body ?? '', timestamp: message.timestamp, type: message.type, }); return { status: 'ok' }; // Always return 200 fast }
whatsapp/ai-parser.processor.ts — BullMQ Worker
TypeScript
@Processor('ai.parser') export class AiParserProcessor extends WorkerHost { async process(job: Job<InboundMessageJob>): Promise<void> { const { phoneNumberId, waId, body, messageId } = job.data; // 1. Resolve tenant from phone number ID const tenant = await this.tenantRepo.findByWaPhoneId(phoneNumberId); if (!tenant) return; // 2. Find or create customer let customer = await this.customerRepo.findByWaId(waId, tenant.id); if (!customer) { customer = await this.customerRepo.create({ waId, tenantId: tenant.id }); } // 3. Store raw message await this.waMessageRepo.save({ waMessageId: messageId, body, direction: 'inbound', customerId: customer.id, tenantId: tenant.id }); // 4. Call AI to extract order intent const parsed = await this.aiService.parseOrderIntent(body, customer); if (parsed.isOrder) { // 5. Create draft order const order = await this.ordersService.create({ tenantId: tenant.id, customerId: customer.id, styleName: parsed.style, deadline: parsed.deadline, notes: parsed.notes, source: 'whatsapp', waThreadId: waId, }); // 6. Send confirmation back on WhatsApp await this.waService.sendMessage(tenant, waId, `✅ Order ${order.orderRef} received!\n\nStyle: ${parsed.style}\nDeadline: ${parsed.deadlineFormatted}\n\nReply with your measurements or type MEASURES to reuse saved ones.` ); } } }
Automation Engine

Event-trigger
revenue system

Architecture: A cron job runs every 15 minutes across all tenants. It evaluates each enabled automation_trigger, queries eligible targets, and pushes WhatsApp messages via BullMQ with deduplication (Redis SETNX per customer+trigger+day).
TRIGGER TYPE 01
Customer Inactive
WHEN customer.last_order_at < NOW() - INTERVAL '30 days' AND customer has no active order AND not messaged in 14 days
Send re-engagement WhatsApp: "Hey [Name]! Ready for your next look? 👗"
TRIGGER TYPE 02
Order Ready
WHEN order.current_stage = 'ready' AND payment.status = 'pending' AND no message sent in last 24h
Send payment link + pickup notice via WhatsApp with Paystack link
TRIGGER TYPE 03
Unpaid Balance
WHEN payment.type = 'balance' AND payment.status = 'pending' AND order.current_stage = 'ready' AND link_sent_at < NOW() - INTERVAL '48h'
Escalating reminder: Day 2 gentle, Day 5 firm, Day 10 final notice
TRIGGER TYPE 04
Deadline Risk
WHEN order.deadline < NOW() + INTERVAL '48h' AND order.current_stage IN ('received','cutting') AND assigned_to is NOT null
Alert staff member via WhatsApp: "⚠️ Order SEW-0042 is at risk"
TRIGGER TYPE 05
Birthday Upsell
WHEN customer.birthday = CURRENT_DATE + INTERVAL '14 days' AND customer.total_orders > 0
Birthday promo WhatsApp: "Special rate for your birthday look 🎂"
TRIGGER TYPE 06
Repeat Customer Upsell
WHEN order marked as 'delivered' AND customer.total_orders >= 3 AND last upsell message > 60 days ago
Send loyalty message + suggest complementary styles from past orders
automation/automation.scheduler.ts
TypeScript
@Injectable() export class AutomationScheduler { @Cron('*/15 * * * *') // Every 15 minutes async runAutomationCycle() { const tenants = await this.tenantRepo.findActive(); for (const tenant of tenants) { const triggers = await this.triggerRepo.findEnabled(tenant.id); for (const trigger of triggers) { const targets = await this.triggerEvaluator.evaluate(trigger, tenant.id); for (const target of targets) { // Deduplication — never double-fire same trigger for same customer same day const dedupKey = `automation:${trigger.id}:${target.customerId}:${dayjs().format('YYYY-MM-DD')}`; const alreadySent = await this.redis.get(dedupKey); if (alreadySent) continue; await this.waQueue.add('send-automation', { tenantId: tenant.id, customerId: target.customerId, triggerId: trigger.id, message: this.templateEngine.render(trigger.messageTemplate, target), }, { attempts: 3, backoff: { type: 'exponential', delay: 5000 } }); await this.redis.set(dedupKey, 1, 'EX', 86400); } } } } }
Frontend Dashboard

Dashboard UI
structure & preview

Next.js 14 App Router. Tailwind CSS. Mobile-first. Three-panel layout: sidebar nav, main content, context panel. WhatsApp serves as the primary interface — dashboard is for power users and reports only.

app.sewops.ng/dashboard
📋  Today
📦  Orders
👤  Customers
💳  Payments
⚡  Automation
📊  Reports
⚙️  Settings
Good morning, Funmi 👋
Sunday, 19 April 2026  ·  Lagos, NG
12
Active Orders
+3 this week
4
Due This Week
2 urgent
₦82k
Pending Payments
3 unpaid
₦340k
This Month
↑ 18% vs last
TODAY'S ACTIVE ORDERS + New Order
Amara Okonkwo
Ankara Midi Dress
CUTTING
Apr 21
Ngozi Eze
Iro and Buba Set
SEWING
Apr 24
Chidinma Adeyemi
Evening Gown
FITTING
Apr 22
Blessing Okeke
Agbada (Couple)
READY
Apr 19
// app/ (Next.js 14 App Router) app/ ├── (auth)/ │ ├── login/page.tsx │ └── register/page.tsx ├── (dashboard)/ │ ├── layout.tsx ← sidebar + topbar │ ├── page.tsx ← today view │ ├── orders/ │ │ ├── page.tsx ← pipeline kanban │ │ ├── [id]/page.tsx ← order detail │ │ └── new/page.tsx ← quick create │ ├── customers/ │ │ ├── page.tsx ← customer list │ │ └── [id]/page.tsx ← profile + measurements │ ├── payments/page.tsx ← paid/unpaid split │ ├── automation/page.tsx ← trigger config │ └── reports/page.tsx ← revenue charts ├── api/ ← thin proxy to NestJS └── onboarding/ ├── step-1/page.tsx ← business info ├── step-2/page.tsx ← WhatsApp connect └── step-3/page.tsx ← first order
components/ ├── orders/ │ ├── PipelineKanban.tsx ← drag-drop stage view │ ├── OrderCard.tsx ← compact order display │ ├── OrderDetailDrawer.tsx ← slide-in panel │ └── QuickOrderForm.tsx ← minimal 5-field form ├── customers/ │ ├── MeasurementCard.tsx ← visual body diagram │ ├── CustomerHistory.tsx ← timeline of orders │ └── WhatsAppThread.tsx ← message log ├── payments/ │ ├── PaymentLinkButton.tsx ← one-click generate + send │ └── RevenueChart.tsx ← recharts line chart ├── automation/ │ └── TriggerToggle.tsx ← on/off per trigger └── ui/ ├── StageChip.tsx ← colored stage badge ├── UrgencyBadge.tsx ← deadline indicator └── WaSendButton.tsx ← send to WhatsApp
// Zustand stores — minimal, focused // useOrderStore — pipeline state const useOrderStore = create()({ orders: Order[], todayOrders: Order[], urgentOrders: Order[], updateStage: (orderId, stage) => void, fetchToday: () => Promise<void>, }); // useCustomerStore — profiles + measurements const useCustomerStore = create()({ customers: Customer[], currentCustomer: Customer | null, measurements: Measurement[], fetchMeasurements: (customerId) => Promise<void>, }); // usePaymentStore — financial tracking const usePaymentStore = create()({ unpaidOrders: Order[], monthRevenue: number, generateLink: (orderId) => Promise<string>, sendReminder: (paymentId) => Promise<void>, }); // React Query for server state; Zustand for UI state only
Deployment

AWS infrastructure
setup

Compute
EC2 — API Server
t3.medium (2 vCPU, 4GB RAM)
Ubuntu 22.04 LTS
PM2 cluster mode
Auto-scaling group
Min: 1 / Max: 4 instances
Database
RDS PostgreSQL 15
db.t3.medium instance
Multi-AZ for production
Automated backups (7 days)
Connection pooling via PgBouncer
Max connections: 200
Cache + Queue
ElastiCache Redis 7
cache.t3.micro (dev)
cache.t3.small (prod)
BullMQ job queues
Session store
Dedup keys (24h TTL)
Storage
S3 — Media & Docs
Private bucket per tenant
Pre-signed URLs for upload
CloudFront CDN distribution
Lifecycle: 90-day transition to IA
SSE-S3 encryption
Frontend
Vercel (Next.js)
Automatic CI/CD from main
Edge functions for API proxy
Preview deployments per PR
Custom domain: app.sewops.ng
Built-in CDN + Analytics
Edge / DNS
Cloudflare
DNS for sewops.ng
WAF + DDoS protection
TLS 1.3 everywhere
Webhook traffic: bypass cache
Rate limit: 100 req/min/IP
Secrets
AWS Secrets Manager
DB credentials
WA access tokens (per tenant)
Paystack/Flutterwave keys
Auto-rotation enabled
Least-privilege IAM roles
Monitoring
CloudWatch + Sentry
API latency alerts (>300ms)
Error rate dashboards
BullMQ queue depth metrics
Sentry for error tracking
PagerDuty on-call integration
docker-compose.yml — local dev
YAML
services: api: build: ./apps/api ports: ["3001:3001"] environment: DATABASE_URL: postgres://sewops:local@db:5432/sewops REDIS_URL: redis://redis:6379 JWT_SECRET: "local-dev-secret" depends_on: [db, redis] volumes: [./apps/api:/app, /app/node_modules] db: image: postgres:15-alpine environment: POSTGRES_DB: sewops POSTGRES_USER: sewops POSTGRES_PASSWORD: local ports: ["5432:5432"] volumes: [pgdata:/var/lib/postgresql/data] redis: image: redis:7-alpine ports: ["6379:6379"] volumes: pgdata:
Build Plan

45-day
sprint roadmap

1
Foundation
Days 1–15
  • Day 1–2: Repo setup. Monorepo (Turborepo). NestJS API + Next.js scaffolded. CI/CD pipeline.
  • Day 3–4: Auth module. JWT + refresh. Tenant middleware. Role guards.
  • Day 5–6: PostgreSQL schema. All migrations via TypeORM. Seed data for dev.
  • Day 7–8: WhatsApp Cloud API integration. Webhook verify + receive. Test with Meta sandbox.
  • Day 9–10: Customers + Measurements modules. CRUD + versioned measurements.
  • Day 11–12: Orders module. Full CRUD + stage transitions + event emissions.
  • Day 13–14: BullMQ setup. Redis. ai.parser queue + basic intent detection.
  • Day 15: End-to-end test: WhatsApp → parse → order created → confirmation sent.
2
Core Product
Days 16–30
  • Day 16–17: Payments module. Paystack integration. Link generation. Webhook reconciliation.
  • Day 18–19: Deposit-first logic. Auto-send link on order create. Balance reminder queue.
  • Day 20–21: Next.js dashboard shell. Sidebar layout. Auth flow. Today view.
  • Day 22–23: Pipeline kanban UI. Drag-drop stage updates. Urgency indicators.
  • Day 24–25: Customer profile pages. Measurement input UI. Order history timeline.
  • Day 26–27: Automation engine. Cron scheduler. 4 core triggers live.
  • Day 28–29: Reports module. Revenue chart. Unpaid orders view. Top customers.
  • Day 30: Internal beta with 3 real tailors. Feedback session. Bug triage.
3
Launch
Days 31–45
  • Day 31–32: Onboarding flow. 3-step wizard. WhatsApp QR connect. First order demo.
  • Day 33–34: Mobile PWA polish. Touch-optimized pipeline. WhatsApp deep links.
  • Day 35–36: Production AWS setup. RDS, ElastiCache, EC2. Cloudflare DNS.
  • Day 37–38: Flutterwave as fallback payment. Multi-provider abstraction tested.
  • Day 39–40: Security hardening. Rate limiting. Tenant isolation audit. Pen test basics.
  • Day 41–42: Monitoring. CloudWatch dashboards. Sentry. Alert thresholds set.
  • Day 43–44: Expanded beta: 20 tailors. Guided onboarding. WhatsApp support group.
  • Day 45: 🚀 Public launch. Landing page live. First paid plan activated.
Success Metrics

What
winning looks like

<5min
Time to First Order
70%+
Day-7 Retention
60%+
Deposit Conversion
40%↑
Revenue per Tailor
The moat: After 7 days, a tailor has all their customer measurements in SewOps. Their WhatsApp replies auto-create orders. Payments arrive before they even ask. The cost of switching is now zero-to-negative — moving means manually rebuilding their entire customer measurement database. That's the lock-in.
Onboarding Flow

Under 2 minutes
to first order

📱
Sign up with phone number
Verify OTP via SMS
🏪
Enter business name
💬
Connect WhatsApp Business
📋
Create first order (from chat or form)
🎉
Dashboard auto-generated