Read this file before touching any code. Every decision in this project flows from this document. This is the single source of truth for the entire codebase. If something is not documented here — ask before assuming. Never guess.
What this project is: twizrr is a Nigerian social commerce marketplace. Stores post products, lifestyle images, and short text posts to a social feed — shoppers discover and buy through the feed and through a WhatsApp AI shopping assistant. Every transaction is escrow-protected: money is held until the shopper confirms delivery using a 6-digit code, then released automatically to the store's bank account.
How the system works: A NestJS 10 backend API serves two clients — a Next.js 14 web app and a WhatsApp AI assistant. The backend handles all business logic: products, orders, escrow payments via Paystack, payout automation via BullMQ, and AI product search via Vertex AI embeddings stored in pgvector. The web app serves store owners (store workspace) and shoppers (discovery + checkout). WhatsApp serves shoppers only — it is not used for store management.
The core feature: Protected-payment social commerce. Store lists product → shopper discovers via feed or WhatsApp AI → shopper pays → payment is protected by twizrr Buyer Protection → store prepares order → delivery is handled by twizrr → shopper confirms delivery with a delivery code → store receives payment automatically.
External services:
- Paystack — card payments, Dedicated Virtual Accounts (DVA), bank transfer payouts to stores
- Shipbubble — nationwide logistics, delivery booking and tracking
- Meta WhatsApp Cloud API — WhatsApp webhook, 6 pre-approved template messages
- Gemini 2.5 Flash — WhatsApp AI conversation and intent parsing
- Google Cloud Vision API — image labeling for WhatsApp image search + content moderation (SafeSearch)
- Vertex AI Multimodal Embeddings — 1408-dim product vectors stored in pgvector for semantic search
- Africa's Talking — USSD checkout, SMS fallback
- Resend — transactional email (order confirmations, OTPs, payout receipts)
- Cloudinary — image CDN (product photos, store logos, post images)
- Google Places Autocomplete API — store address input during setup
- Prembly — NIN verification (Tier 2 KYB) and CAC verification (Tier 3 KYB)
- Neon — managed PostgreSQL with pgvector extension and PgBouncer connection pooling
- Upstash — managed Redis for caching, BullMQ job queues, rate limiting
What this project is NOT:
- Not a B2B or wholesale platform — twizrr is B2C only
- Not a hardware or building materials marketplace — general fashion, electronics, food, beauty, home goods
- Not a WhatsApp management tool — WhatsApp is for shoppers buying, not for store owners managing
- Not Swifta — the old codebase was called Swifta. twizrr is a complete rebrand and rebuild
- Not building supplier features — the SupplierProfile model and any B2B flows are out of scope
- Not building RFQ, Quote, TradeFinancing, or any legacy modules from the Swifta codebase
Official social commerce platform for twizrr (CODEDDEVS TECHNOLOGY LTD).
- URL: twizrr.com
- API: api.twizrr.com
- Audience: Nigerian shoppers (buying via WhatsApp and web) and Nigerian store owners (selling via the web store workspace)
- Purpose: Trust infrastructure for Nigerian social commerce — escrow payments, verified stores, and WhatsApp-native shopping
- Tone: Warm, professional, and direct. No pidgin. No slang. No emojis in UI or WhatsApp messages — icons only.
- Critical constraint: Every transaction must be escrow-protected. Money never goes directly from shopper to store. Paystack webhook HMAC must be verified on every payment callback before any order status changes.
Do not change any of these without explicit instruction.
| Layer | Choice |
|---|---|
| Backend framework | NestJS 10 + TypeScript strict mode |
| Frontend framework | Next.js 14 (App Router) + TypeScript |
| Mobile (Phase 3) | Expo SDK 52 + Expo Router v4 — DO NOT BUILD YET |
| Runtime | Node.js 20+ |
| Database | Neon PostgreSQL 16 + pgvector extension |
| ORM | Prisma (migrate deploy on shared envs — never db push) |
| Cache + Queues | Upstash Redis + BullMQ |
| Auth | JWT (HttpOnly cookies) + custom JwtAuthGuard |
| Payments | Paystack (checkout, DVA, transfers, refunds) |
| Logistics | Shipbubble API |
| Resend | |
| SMS / USSD | Africa's Talking |
| Images | Cloudinary (with Cloud Vision SafeSearch before upload) |
| AI — conversation | Gemini 2.5 Flash (via Google AI SDK) |
| AI — vision | Google Cloud Vision API |
| AI — embeddings | Vertex AI Multimodal Embeddings (1408-dim) |
| AI — search | pgvector (cosine similarity on ProductEmbedding table) |
| KYB verification | Prembly API (NIN + CAC) |
| Address input | Google Places Autocomplete API |
| Hosting — backend | Provider-neutral Node.js 20+ service with long-running process support for queues |
| Hosting — frontend | Provider-neutral Next.js 14 host |
| Package manager | pnpm — NEVER use npm or yarn |
| Monorepo | Turborepo + pnpm workspaces |
| Package scope | @twizrr/ |
| PR review | CodeRabbit (automated) |
Design system is defined in TWIZRR_DESIGN_SYSTEM.md. Final brand assets (logo, exact tokens) are pending from AbdulRaseed (Brand Designer). Do not start frontend visual work until TWIZRR_DESIGN_SYSTEM.md is marked final. Below are the confirmed values for use in all current development.
Saffron: #E8A020 — primary CTA buttons, active states, escrow pill, saffron accents
Espresso: #1C1208 — headings, dark backgrounds, store mode header
Berry: #C2185B — Tier 3 verified badge, accent highlight
Mocha: #6B5B4E — Tier 2 verified badge
Bianca: #FBF7F2 — page background (light mode)
| Element | Font | Weight | Notes |
|---|---|---|---|
| Headings | Syne | 700 | Load via next/font/google |
| Body / UI | Cabinet Grotesque | 400 / 500 | Load via next/font/google |
| Prices | Cabinet Grotesque (system) | 700 | Naira amounts only |
- No emojis anywhere — icons only in all UI surfaces (web, mobile, WhatsApp messages)
- Icons — use Lucide React for all icons. Never inline custom SVG unless it's the twizrr logo
- Colours — always use CSS variables (design tokens), never hardcoded hex values
- Buttons — primary: Saffron background + Espresso text. Secondary: outlined
- Spacing — base unit 4px. Use Tailwind spacing scale consistently
- Radius — small elements 6px, cards 12px, modals 16px, images 20px
- Mobile first — all layouts start at 375px. Minimum touch target 44×44px
- Dark mode — Phase 3. Do not implement dark mode in MVP
- Primary logo SVG:
public/logo/logo.svg— full brand lockup (ZZ mark + TWIZRR wordmark); use for brand-heavy surfaces such as auth, landing, empty/error states, and hero placements - Wordmark SVG:
public/logo/wordmark.svg— text-only horizontal wordmark; use in headers/nav where space is tight - Mark SVG:
public/logo/mark.svg— icon-only ZZ mark; use in favicons, loaders, compact buttons, and small spaces
twizrr/ # monorepo root
├── apps/
│ ├── backend/ # NestJS 10 API
│ │ ├── src/
│ │ │ │
│ │ │ ├── core/ # PLATFORM INFRASTRUCTURE — no business logic
│ │ │ │ ├── config/ # ConfigModule per service (app, db, redis, paystack...)
│ │ │ │ ├── prisma/ # PrismaService (extends PrismaClient)
│ │ │ │ ├── redis/ # RedisService wrapper (Redis protocol)
│ │ │ │ ├── logger/ # nestjs-pino structured logging
│ │ │ │ ├── throttler/ # ThrottlerModule setup (rate limiting)
│ │ │ │ ├── health/ # GET /health — UptimeRobot pings this
│ │ │ │ └── core.module.ts # imports + exports all of the above
│ │ │ │
│ │ │ ├── channels/ # EXTERNAL ENTRY POINTS — not business logic
│ │ │ │ ├── whatsapp/ # Meta WhatsApp Cloud API channel (WIZZA)
│ │ │ │ │ ├── whatsapp.module.ts
│ │ │ │ │ ├── whatsapp.controller.ts # GET + POST /whatsapp/webhook
│ │ │ │ │ ├── whatsapp.processor.ts # BullMQ consumer (async processing)
│ │ │ │ │ ├── whatsapp.service.ts # message routing + intent handling
│ │ │ │ │ ├── whatsapp-session.service.ts # consent + session per phone
│ │ │ │ │ ├── whatsapp-intent.service.ts # Gemini function-calling
│ │ │ │ │ ├── whatsapp-auth.service.ts # WhatsApp account linking flows
│ │ │ │ │ ├── whatsapp-product-discovery.service.ts # text discovery + canonical URLs
│ │ │ │ │ ├── whatsapp-search-ranking.ts # hybrid ranking helpers
│ │ │ │ │ ├── image-search.service.ts # embedding-first image search
│ │ │ │ │ ├── whatsapp-interactive.service.ts # list messages, buttons, text sends
│ │ │ │ │ ├── whatsapp-analytics.service.ts # WhatsAppAnalytics v2 logging
│ │ │ │ │ ├── whatsapp-logger.service.ts
│ │ │ │ │ ├── whatsapp.constants.ts
│ │ │ │ │ ├── whatsapp.utils.ts # phone normalize + log masking
│ │ │ │ │ └── AGENTS.md # WhatsApp-specific build instructions
│ │ │ │ │
│ │ │ │ └── ussd/ # Africa's Talking USSD channel
│ │ │ │ ├── ussd.module.ts
│ │ │ │ ├── ussd.controller.ts # POST /ussd/callback
│ │ │ │ └── ussd.service.ts
│ │ │ │
│ │ │ ├── integrations/ # THIRD-PARTY PROVIDER CLIENTS
│ │ │ │ ├── paystack/ # Paystack — checkout, DVA, transfers, refunds
│ │ │ │ │ ├── paystack.module.ts
│ │ │ │ │ ├── paystack.client.ts # all Paystack API calls (one place)
│ │ │ │ │ └── paystack.types.ts
│ │ │ │ ├── cloudinary/ # Image CDN + upload
│ │ │ │ │ ├── cloudinary.module.ts
│ │ │ │ │ └── cloudinary.client.ts
│ │ │ │ ├── resend/ # Transactional email
│ │ │ │ │ ├── resend.module.ts
│ │ │ │ │ └── resend.client.ts
│ │ │ │ ├── shipbubble/ # Logistics + delivery booking
│ │ │ │ │ ├── shipbubble.module.ts
│ │ │ │ │ └── shipbubble.client.ts
│ │ │ │ ├── africastalking/ # SMS + USSD
│ │ │ │ │ ├── africastalking.module.ts
│ │ │ │ │ └── africastalking.client.ts
│ │ │ │ ├── prembly/ # KYB — NIN + CAC verification
│ │ │ │ │ ├── prembly.module.ts
│ │ │ │ │ └── prembly.client.ts
│ │ │ │ ├── google-places/ # Store address autocomplete
│ │ │ │ │ ├── google-places.module.ts
│ │ │ │ │ └── google-places.client.ts
│ │ │ │ └── ai/ # All AI provider clients (one place)
│ │ │ │ ├── ai.module.ts
│ │ │ │ ├── gemini.client.ts # Gemini 2.5 Flash
│ │ │ │ ├── vision.client.ts # Google Cloud Vision API
│ │ │ │ └── vertex.client.ts # Vertex AI Multimodal Embeddings
│ │ │ │
│ │ │ ├── domains/ # BUSINESS LOGIC — grouped by domain area
│ │ │ │ │
│ │ │ │ ├── commerce/ # Products, posts, feed, search, sourcing
│ │ │ │ │ ├── product/ # Product CRUD, variants, stock display
│ │ │ │ │ ├── inventory/ # InventoryEvent (append-only), ProductStockCache
│ │ │ │ │ ├── sourced-product/ # Dropship sourcing — floor enforcement
│ │ │ │ │ ├── post/ # PRODUCT_POST, IMAGE_POST, GIST, TWIZZ
│ │ │ │ │ ├── feed/ # Algorithm: For You, Following, Stores, Explore
│ │ │ │ │ ├── search/ # Hybrid: pgvector + tsvector + metadata + trust
│ │ │ │ │ ├── size-guide/ # Platform-managed guides (seeded — store owners cannot create)
│ │ │ │ │ └── commerce.module.ts
│ │ │ │ │
│ │ │ │ ├── orders/ # Full order lifecycle
│ │ │ │ │ ├── order/ # State machine: PENDING_PAYMENT → COMPLETED
│ │ │ │ │ ├── cart/ # CartItem management
│ │ │ │ │ ├── delivery/ # Shipbubble + delivery preview API
│ │ │ │ │ ├── dispute/ # Dispute filing, 72hr window, OPERATOR queue
│ │ │ │ │ └── orders.module.ts
│ │ │ │ │
│ │ │ │ ├── money/ # ALL money movement — single domain boundary
│ │ │ │ │ ├── ledger/ # LedgerEntry (append-only source of truth)
│ │ │ │ │ ├── payment/ # Paystack checkout, DVA, webhook processing
│ │ │ │ │ ├── payout/ # Bank transfers to stores (BullMQ — never sync)
│ │ │ │ │ ├── escrow/ # Escrow hold + release logic
│ │ │ │ │ ├── refund/ # Refund triggers + Paystack Refunds API
│ │ │ │ │ └── money.module.ts
│ │ │ │ │
│ │ │ │ ├── users/ # Users, stores, trust, verification
│ │ │ │ │ ├── auth/ # JWT, signup, OTP, refresh tokens, sessions
│ │ │ │ │ ├── user/ # User CRUD, profile, measurements, addresses
│ │ │ │ │ ├── store/ # StoreProfile CRUD, settings, KYB flows
│ │ │ │ │ ├── verification/ # KYB — Prembly NIN + CAC flows
│ │ │ │ │ ├── follow/ # Follow relationships (users + stores)
│ │ │ │ │ └── users.module.ts
│ │ │ │ │
│ │ │ │ ├── social/ # Social interactions and communications
│ │ │ │ │ ├── notification/ # In-app notifications + email dispatch
│ │ │ │ │ ├── realtime/ # socket.io push gateway (notification:new today; chat rides on it later)
│ │ │ │ │ ├── chat/ # NOT BUILT YET — placeholder; will use realtime/ gateway
│ │ │ │ │ └── social.module.ts
│ │ │ │ │
│ │ │ │ └── platform/ # Operational and admin concerns
│ │ │ │ ├── admin/ # OPERATOR + SUPER_ADMIN tools, AuditLog
│ │ │ │ ├── moderation/ # Cloud Vision decisions + OPERATOR review queue
│ │ │ │ ├── upload/ # Cloudinary — ALWAYS runs moderation first
│ │ │ │ ├── embedding/ # Vertex AI embedding generation + pgvector storage
│ │ │ │ └── platform.module.ts
│ │ │ │
│ │ │ ├── queue/ # BULLMQ — all queues and processors
│ │ │ │ ├── queue.constants.ts # named queue string constants
│ │ │ │ ├── queue.module.ts # registers all 16 BullMQ queues
│ │ │ │ └── processors/ # one processor file per queue
│ │ │ │ ├── embedding.processor.ts
│ │ │ │ ├── payout.processor.ts
│ │ │ │ ├── payout-retry.processor.ts
│ │ │ │ ├── notification.processor.ts
│ │ │ │ ├── auto-confirm.processor.ts
│ │ │ │ ├── auto-confirm-warning.processor.ts
│ │ │ │ ├── floor-check.processor.ts
│ │ │ │ ├── moderation.processor.ts
│ │ │ │ ├── order-expiry.processor.ts
│ │ │ │ ├── restock-alert.processor.ts
│ │ │ │ ├── reconciliation.processor.ts
│ │ │ │ ├── review-prompt.processor.ts
│ │ │ │ ├── session-cleanup.processor.ts
│ │ │ │ └── floor-check.processor.ts
│ │ │ │
│ │ │ ├── common/ # SHARED UTILITIES — no business logic
│ │ │ │ ├── guards/
│ │ │ │ │ ├── jwt-auth.guard.ts
│ │ │ │ │ └── roles.guard.ts
│ │ │ │ ├── decorators/
│ │ │ │ │ ├── current-user.decorator.ts
│ │ │ │ │ └── roles.decorator.ts
│ │ │ │ ├── filters/
│ │ │ │ │ └── global-exception.filter.ts
│ │ │ │ ├── interceptors/
│ │ │ │ │ └── response-transform.interceptor.ts
│ │ │ │ ├── pipes/
│ │ │ │ │ └── validation.pipe.ts
│ │ │ │ └── dto/
│ │ │ │ └── pagination.dto.ts
│ │ │ │
│ │ │ └── app.module.ts # CLEAN TOP-LEVEL COMPOSITION ONLY
│ │ │ # imports: CoreModule, QueueModule,
│ │ │ # CommerceDomainModule, OrdersDomainModule,
│ │ │ # MoneyDomainModule, UsersTrustDomainModule,
│ │ │ # SocialDomainModule, PlatformDomainModule,
│ │ │ # WhatsAppModule, USSDModule
│ │ │
│ │ ├── prisma/
│ │ │ ├── schema.prisma # source of truth for all models
│ │ │ └── seed.ts # seeds SizeGuide records at first deployment
│ │ └── AGENTS.md # backend-specific instructions — read after root
│ │
│ └── web/ # Next.js 14 App Router
│ ├── src/
│ │ ├── app/ # all routes (App Router)
│ │ │ ├── (public)/ # no auth: /, /explore, /p/[code], /@[handle]
│ │ │ ├── (auth)/ # /login, /register, /verify-email
│ │ │ ├── (shopper)/ # /buyer/* — authenticated shopper pages
│ │ │ └── (store)/ # /store/* — authenticated store owner pages
│ │ ├── components/ # shared UI components
│ │ ├── hooks/ # custom React hooks
│ │ ├── lib/ # API client, utilities, constants
│ │ └── styles/ # global CSS + design tokens
│ └── AGENTS.md # web-specific instructions — read after root
│
├── packages/
│ └── shared/ # @twizrr/shared
│ └── src/
│ ├── types/ # shared TypeScript types
│ ├── enums/ # shared enums
│ └── dtos/ # shared DTOs
│
├── AGENTS.md # THIS FILE — read first, always
├── turbo.json
├── pnpm-workspace.yaml
└── docker-compose.yml # local PostgreSQL + Redis
FOUR LAYERS — each with a clear purpose:
core/ PLATFORM INFRASTRUCTURE
Config, Prisma, Redis, Logger, Throttler, Health
No business logic. Setup once, reused everywhere.
AppModule stays clean — not polluted with infra.
channels/ EXTERNAL ENTRY POINTS
WhatsApp and USSD are channels, not features.
They receive external input and route it to domains.
Phase 3: extract whatsapp/ to a dedicated worker service
with zero refactoring — boundaries are already correct.
integrations/ THIRD-PARTY PROVIDER CLIENTS
All Paystack API calls live in integrations/paystack/
All AI API calls live in integrations/ai/
Provider changes (API version, key rotation, replacement)
affect exactly one file. Business logic is untouched.
Mocking in tests: mock the integration client — done.
domains/ BUSINESS LOGIC — grouped by domain
commerce/ — products, posts, feed, search, sourcing
orders/ — order lifecycle, cart, delivery, disputes
money/ — ledger (single source of truth), payments,
payouts, escrow, refunds
users/ — auth, profiles, stores, KYB, follows
social/ — notifications, chat
platform/ — admin, moderation, upload, embeddings
AppModule imports core, queues, and the domain aggregators.
New engineers understand the system in 10 minutes.
money/ DOMAIN IS ESPECIALLY CRITICAL:
LedgerEntry is the single source of truth for all money.
payment/, payout/, escrow/, refund/ all write through
ledger/LedgerService — never create LedgerEntry directly.
Reconciliation, audit, and compliance all query one table.
This is non-negotiable for financial correctness.
Full schema is defined in
apps/backend/prisma/schema.prisma. Full model documentation is inTWIZRR_DATA_ARCHITECTURE.md. Below are the critical models and rules every agent must know.
ALL monetary values: BigInt in kobo (never Float)
₦24,500 = 2450000n
Display as Naira ONLY in frontend — never in backend
ALL primary keys: UUID (cuid() in Prisma)
ALL phone numbers: E.164 format (+2348012345678)
LedgerEntry: APPEND-ONLY — never UPDATE or DELETE any row
Row-level security enforces this at PostgreSQL level
InventoryEvent: APPEND-ONLY — never UPDATE or DELETE any row
SizeGuide records: NEVER created by store owners
Seeded by prisma/seed.ts at deployment only
SourcedProduct.sellingPriceKobo:
MUST be >= product.dropshipperPriceKobo OR product.retailPriceKobo
DB CHECK constraint enforces this
API must also validate before insert/update
Error code: PRICE_BELOW_FLOOR
Order.orderCode format: TWZ-XXXXXX (6 random digits)
e.g. TWZ-384729
Never use any other format
StoreProfile.storeType (DIGITAL | PHYSICAL):
NEVER return this field in any shopper-facing API response
Internal only — affects routing and logistics, not public display
SourcedProduct.physicalStoreId and physicalProductId:
NEVER return in any shopper-facing API response
Physical store is always invisible to shoppers
User — universal account (shopper + store owner)
StoreProfile — attached to User when they open a store
Product — store's listed item (listing IS inventory)
ProductVariant — size + color combinations per product
ProductImage — up to 5 images per product, with variant assignment
ProductSizeGuideConfig — links product to platform size guide
ProductEmbedding — 1408-dim Vertex AI vector in pgvector
InventoryEvent — append-only stock event log
ProductStockCache — fast current stock reads per variant
SourcedProduct — digital store's listing of a physical store product
Post — PRODUCT_POST | IMAGE_POST | GIST | TWIZZ
Order — full order lifecycle
CartItem — shopper's cart (persisted to DB)
Payment — Paystack payment record
Payout — bank transfer to store
LedgerEntry — append-only financial audit log
SizeGuide — platform-managed (seeded, not store-created)
ModerationLog — every Cloud Vision decision recorded
WhatsAppAnalytics — AI interaction signals (captured from day 1)
AuditLog — every OPERATOR/ADMIN action recorded
# DATABASE
DATABASE_URL= # Neon pooled connection (app queries via PgBouncer)
DIRECT_URL= # Neon direct connection (migrations only — never for app)
# REDIS
REDIS_URL= # Redis protocol URL (redis:// or rediss://) for BullMQ + caching + rate limiting
# AUTH
JWT_SECRET= # JWT signing secret — rotate if exposed
JWT_EXPIRES_IN= # e.g. 15m (access token)
JWT_REFRESH_EXPIRES_IN= # e.g. 7d (refresh token)
# PAYSTACK
PAYSTACK_SECRET_KEY= # Live secret key — server-side only, never client
PAYSTACK_PUBLIC_KEY= # Publishable key — safe for client
PAYSTACK_WEBHOOK_SECRET=# HMAC signature secret for webhook verification
# CLOUDINARY
CLOUDINARY_CLOUD_NAME= # Cloud name
CLOUDINARY_API_KEY= # API key
CLOUDINARY_API_SECRET= # API secret — server-side only
# GOOGLE AI
GEMINI_API_KEY= # From Google AI Studio — Gemini 2.5 Flash
GOOGLE_CLOUD_PROJECT= # GCP project ID (twizrr-ai)
GOOGLE_APPLICATION_CREDENTIALS= # Path to service account JSON
# META / WHATSAPP
WHATSAPP_TOKEN= # Meta Cloud API access token
WHATSAPP_APP_SECRET= # For webhook HMAC verification
WHATSAPP_VERIFY_TOKEN= # Webhook verification token (set in Meta dashboard)
WHATSAPP_PHONE_NUMBER_ID= # The phone number ID for the WhatsApp account
# AFRICA'S TALKING
AT_USERNAME= # Africa's Talking username
AT_API_KEY= # Africa's Talking API key
AT_SENDER_ID= # SMS sender ID (e.g. twizrr)
# RESEND
RESEND_API_KEY= # Resend email API key
# PREMBLY
PREMBLY_API_KEY= # Prembly KYB API key
PREMBLY_APP_ID= # Prembly app ID
# SHIPBUBBLE
SHIPBUBBLE_API_KEY= # Shipbubble logistics API key
# GOOGLE PLACES
GOOGLE_PLACES_API_KEY= # For store address autocomplete
# APP
NODE_ENV= # development | staging | production
PORT= # Backend port (default 4000)
CORS_ORIGINS= # Comma-separated allowed origins
APP_URL= # Backend base URL (e.g. https://api.twizrr.com)
WEB_URL= # Frontend base URL (e.g. https://twizrr.com)Full route map is in
apps/backend/AGENTS.md. All routes follow this envelope:
// Success
{ "success": true, "data": {}, "message": "optional" }
// Error
{ "success": false, "message": "Human-readable error", "code": "ERROR_CODE" }/auth/* — signup, login, OTP, refresh token (public)
/users/* — user profile, measurements, addresses (protected)
/stores/* — store CRUD, settings, KYB (protected — store owners)
/products/* — product CRUD, search (mixed public/protected)
/sourced/* — sourcing catalogue, SourcedProduct CRUD (protected — digital stores)
/posts/* — post CRUD, feed (mixed)
/feed/* — personalised feed algorithm (protected)
/search/* — hybrid product/store/post search (mixed)
/cart/* — CartItem management (protected — shoppers)
/orders/* — order lifecycle (protected)
/payments/* — Paystack checkout, DVA (protected)
/payouts/* — payout history, bank details (protected — store owners)
/delivery/* — Shipbubble, delivery preview (mixed)
/whatsapp/* — webhook handler (public — HMAC verified)
/upload/* — Cloudinary upload with moderation (protected)
/notifications/* — in-app notifications (protected; live push via socket.io `notification:new`)
/chat/* — NOT BUILT YET (will be REST + the socket.io realtime gateway)
/verification/* — KYB flows, Prembly (protected — store owners)
/disputes/* — dispute filing and management (protected)
/admin/* — OPERATOR + SUPER_ADMIN only
/health — health check (public — UptimeRobot)
// Auth is NOT a global guard in NestJS
// Routes without @UseGuards(JwtAuthGuard) are PUBLIC by default
// The only global guard is ThrottlerGuard (rate limiting)
// Protected route pattern:
@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@CurrentUser() user: User) { }
// Public route (no guard needed — just no decorator):
@Get('products')
getProducts() { }
// Role-based:
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles(UserRole.SUPER_ADMIN)
@Get('admin/users')
getUsers() { }
// Rate limit bypass (for webhooks):
@SkipThrottle()
@Post('whatsapp/webhook')
handleWebhook() { }
// Roles in system:
// USER — all regular accounts (shoppers + store owners)
// OPERATOR — moderation, disputes, payout monitoring
// SUPER_ADMIN — full platform control
// SUPPORT — read-only accessFull MVP specification is in
TWIZRR_MVP_SCOPE.md. Everything in that document must work before launch. Below is the quick reference for agents.
- User signup + store setup
- Product listing (4-screen form with variants, size guide, specs)
- Source Products catalogue + SourcedProduct (dropship flow)
- Paystack escrow payments (card + DVA)
- Order lifecycle + dispatch + 6-digit delivery OTP
- WhatsApp AI assistant (Gemini + Cloud Vision + Vertex AI + pgvector)
- 6 Meta WhatsApp template messages
- Cloud Vision SafeSearch moderation (before every Cloudinary upload)
- Vertex AI embedding generation (BullMQ job after product listed)
- KYB verification (Tier 1 auto, Tier 2 NIN via Prembly)
- Store mode workspace (Home/feed, My Store, Products, Orders, Payouts, Analytics, Settings)
- User profile menu (Orders, Cart, Wishlist, Saved, Profile, Settings, etc.)
- Shipbubble delivery integration + delivery preview on product page
- Platform-managed size guides (SizeGuide seeded at deployment)
- Basic dispute filing
- In-app notifications + email (Resend)
- Admin moderation queue + dispute resolution tools
- Mobile app (Expo) — web only at MVP
- Twizz (video posts) — show "Coming Soon" tab only
- Review system — deferred (needs order history first)
- Inspection system — Mustakheem manually verifies at MVP
- Premium subscription — show "Coming Soon" in store menu
- Push notifications — email + WhatsApp templates only
- Size recommendation (user measurements → system suggests)
- Advanced analytics charts — basic numbers only
- Drops creation system — entry point only, not functional
- Tier 3 CAC verification (Prembly) — Tier 1 + Tier 2 only at MVP
- SMS fallback — Africa's Talking SMS deferred to Phase 3
- Supplier features (B2B) — never, not in any phase
- RFQ, Quote, TradeFinancing modules — delete from codebase
These are non-negotiable. Violating these will cause financial or trust damage.
RULE 1 — MONEY IS ALWAYS BIGINT KOBO:
All monetary values: BigInt in kobo
₦24,500 = 2450000n (BigInt)
Never use Float, Decimal, or Number for money
Convert to Naira ONLY in frontend for display
RULE 2 — PAYSTACK WEBHOOK MUST BE VERIFIED:
Every Paystack callback: verify HMAC signature FIRST
If signature invalid: return 400, log, do nothing
Never process payment events without verification
Idempotency keys on all Payment and Payout records
RULE 3 — ESCROW ON EVERY ORDER:
Money never goes directly to store
All payments: held in twizrr Paystack balance
Released only: after 6-digit OTP confirmed OR 48hr auto-confirm
RULE 3A — TWIZRR OWNS DELIVERY:
All marketplace delivery is Twizrr-managed by default
Checkout does not expose old store-delivery or personal-delivery choices
Checkout requires a delivery address and uses User.phone as the default primary delivery phone
Checkout may collect an optional secondary delivery phone
Shopper delivery phones are private platform data
Store owners do not see shopper phone numbers by default
Delivery phone/address may be shared only with approved logistics providers when required
Store owners prepare orders and trigger Ready for pickup / Book delivery through twizrr
Delivery fee is calculated by twizrr from fulfillment pickup point to shopper dropoff
Physical product pickup = physical store pickup address
Dropship pickup = source physical supplier pickup address, never digital dropshipper address
RULE 4 — STORE TYPE IS NEVER PUBLIC:
StoreProfile.storeType (DIGITAL | PHYSICAL)
Remove from ALL shopper-facing API responses
DTOs must exclude this field for public endpoints
Violation exposes business model to competitors
RULE 5 — PHYSICAL STORE IS ALWAYS INVISIBLE FOR SOURCED PRODUCTS:
SourcedProduct.physicalStoreId: NEVER in shopper-facing response
SourcedProduct.physicalProductId: NEVER in shopper-facing response
Shopper always sees the digital store as seller
RULE 6 — DROPSHIPPER PRICE FLOOR (HARD RULE):
SourcedProduct.sellingPriceKobo MUST be >= floor
Floor = product.dropshipperPriceKobo OR product.retailPriceKobo
Enforce at:
1. DB: CHECK constraint in schema
2. API: validate before insert/update, throw 400 PRICE_BELOW_FLOOR
3. Frontend: input minimum + button disabled state
RULE 7 — PLATFORM FEE ON DROPSHIP ORDERS:
2% on FULL order value — charged ONCE
Deducted from digital store margin ONLY
Physical store receives full wholesale amount (no fee)
RULE 8 — CONTENT MODERATION BEFORE CLOUDINARY:
Every image upload must go through Cloud Vision SafeSearch FIRST
BLOCKED images: reject before Cloudinary upload
SENSITIVE: upload with flag — blur for adults, hide from isMinor users
SAFE: upload normally
Never skip this step for any image
RULE 9 — WHATSAPP = SHOPPING ONLY:
WhatsApp AI serves shoppers — never store management
Store owners: web store workspace only
Never add store management endpoints to WhatsApp module
RULE 10 — NO EMOJIS ANYWHERE:
UI: icons only (Lucide React)
WhatsApp messages: plain text only
Push notifications: text only
Email subjects: text only
Violation breaks brand consistency
RULE 11 — LEDER ENTRIES AND INVENTORY EVENTS ARE APPEND-ONLY:
Never UPDATE or DELETE rows in LedgerEntry or InventoryEvent
These are financial + audit records
Row-level security enforces this at DB level
RULE 12 — PRISMA MIGRATIONS:
Development: npx prisma migrate dev --name description
Shared envs (staging, beta, production): npx prisma migrate deploy ONLY
NEVER: npx prisma db push on any shared environment
NEVER: npx prisma migrate dev on staging/beta/production
NEVER: run prisma seed on production
PACKAGE SCOPE: @twizrr/ (not @swifta/, not @hardware-os/)
BRAND NAME: twizrr (lowercase — not Twizrr, not TWIZRR)
In headings/UI: twizrr (always lowercase in copy)
In code: Twizrr (PascalCase for class names)
DATABASE:
Column names: snake_case via Prisma @@map()
Table names: snake_case via Prisma @@map()
Model names: PascalCase in schema.prisma
TYPESCRIPT:
Variables: camelCase
Types/Interfaces: PascalCase
Enums: PascalCase (e.g. OrderStatus.PAID)
Enum values: SCREAMING_SNAKE_CASE (e.g. PENDING_PAYMENT)
Constants: SCREAMING_SNAKE_CASE
API ROUTES: kebab-case (/store/source-products, /buyer/orders)
GIT BRANCHES: kebab-case (feat/product-listing, fix/payout-retry)
ENV VARIABLES: SCREAMING_SNAKE_CASE
NestJS MODULES:
Service: ProductService
Controller: ProductController
Module: ProductModule
DTO: CreateProductDto, UpdateProductDto
Guard: JwtAuthGuard, RolesGuard
Interceptor: ResponseTransformInterceptor
MONEY FIELDS: Always suffix with Kobo (retailPriceKobo, payoutAmountKobo)
IDs: Always suffix with Id (storeId, productId, orderId)
BOOLEANS: Prefix with is/has/can/should (isMinor, hasVariants, isActive)
DATES: Suffix with At (createdAt, verifiedAt, dispatchedAt)
-
TypeScript strict mode always — no
anytypes. Useunknownwith type guards when needed. -
Money is always BigInt — never Float, Decimal, or Number for monetary values. Store in kobo. Display as Naira in frontend only.
-
No
console.login production code — backend: use NestJS Logger (pino). Frontend:if (process.env.NODE_ENV === 'development')guard. -
Validate all input — backend:
class-validator+class-transformeron every DTO. Frontend: Zod schemas. Never trust client input. -
pnpm only — never use npm or yarn. If you see a
package-lock.jsonoryarn.lock— delete it. -
Auth check first — on every protected endpoint, verify JWT before any business logic runs.
-
Paystack HMAC before any payment processing — verify signature on every webhook call. Reject and log if invalid.
-
Cloud Vision before Cloudinary — every image upload must be moderated first. Never upload to Cloudinary without a SAFE or SENSITIVE result from Cloud Vision.
-
Use ORM for all database queries — Prisma only. No raw SQL unless absolutely necessary and approved. Document any raw SQL with a comment explaining why.
-
All API responses use the envelope format:
{ "success": true, "data": {}, "message": "optional" } { "success": false, "message": "error description", "code": "ERROR_CODE" }The
ResponseTransformInterceptorhandles this automatically. Use@Res()only when you need to bypass it (e.g. USSD callbacks, WhatsApp webhook verification). -
BullMQ for all async jobs — never do payouts, notifications, or embeddings synchronously. Always queue them.
-
Idempotency keys on payments — every Payment and Payout record must have an idempotency key. Check before processing to prevent duplicates.
-
Use design tokens, not hardcoded values — frontend: CSS variables only. Never hardcode hex colours, font sizes, or spacing values.
-
Mobile-first CSS — all layouts start at 375px. Use
sm:,md:,lg:Tailwind breakpoints for wider layouts. -
No emojis anywhere — icons only in UI (Lucide React). Plain text in WhatsApp messages. Zero tolerance.
-
Server Components by default in Next.js — use
'use client'only when component needs interactivity (hooks, event handlers, browser APIs). Do not add'use client'to pages or layouts unless required. -
Environment variables — never hardcode any service URL, API key, or secret. Always use
process.env.VARIABLE_NAME. All required vars must be in.env.example. -
Before committing — run all 6 checks:
cd apps/backend && pnpm run lint && npx tsc --noEmit && pnpm run build cd apps/web && pnpm run lint && npx tsc --noEmit && pnpm run build
All six must pass with zero errors before opening a PR.
-
Never push directly to
mainordev— always use feature branches. PRs only. -
If something is not in AGENTS.md or the system documents — ask before building it. Do not invent features, schemas, or business logic. The 30 system documents are the source of truth.
// GLOBAL MIDDLEWARE (main.ts) — do not change without instruction:
// BigInt JSON serialization fix
BigInt.prototype.toJSON = function() { return this.toString() }
// Raw body for Paystack HMAC
app.use(rawBodyMiddleware())
// Structured logging
app.useLogger(logger) // nestjs-pino
// Validation
app.useGlobalPipes(new AppValidationPipe()) // whitelist: true, transform: true
// Exception filter
app.useGlobalFilters(new GlobalExceptionFilter()) // P2002→409, P2025→404
// Response envelope
app.useGlobalInterceptors(new ResponseTransformInterceptor())
// Rate limiting (global guard)
app.useGlobalGuards(new ThrottlerGuard()) // 100 req/min globally
// Security headers
app.use(helmet())
// Cookie parser
app.use(cookieParser())
// CORS
app.enableCors({ origin: CORS_ORIGINS, credentials: true })
// BYPASSING ResponseTransformInterceptor:
// Use @Res() and res.send() directly (USSD callback, WhatsApp webhook)
// BYPASSING ThrottlerGuard:
@SkipThrottle()
// PRISMA ERROR CODES TO HANDLE:
// P2002 → 409 Conflict (unique constraint)
// P2025 → 404 Not Found
// P2003 → 400 Foreign key constraint
// NEON CONNECTION:
// App queries: DATABASE_URL (pooled via PgBouncer)
// Migrations: DIRECT_URL (direct — never pool for migrations)All async jobs go through BullMQ. Never do these synchronously.
| Queue name | Purpose |
|---|---|
embedding-generation |
Vertex AI embedding after product listed |
payout |
Paystack bank transfer to store |
payout-retry |
Retry failed payouts (4 attempts) |
notification |
In-app + email notification dispatch |
whatsapp |
Async WhatsApp message processing |
auto-confirm |
Auto-confirm order 48hrs after dispatch |
auto-confirm-warning |
Warning notification 12hrs before auto-confirm |
floor-check |
Check SourcedProduct prices when floor changes |
moderation |
Async moderation review queue for OPERATOR |
delivery-estimate |
Cache delivery estimates from Shipbubble |
reconciliation |
Hourly Paystack balance vs LedgerEntry check |
restock-alert |
Notify store when variant hits low stock threshold |
order-expiry |
Cancel PENDING_PAYMENT orders after 30 minutes |
review-prompt |
Prompt shopper to review 24hrs after COMPLETED |
session-cleanup |
Remove expired sessions from DB |
audit-flush |
Flush AuditLog buffer to DB |
Store owner publishes product (POST /products)
↓
Product + variants created in DB
InventoryEvent (INITIAL_STOCK) per variant logged
ProductStockCache populated
Post (PRODUCT_POST) created with taggedProductId
↓
BullMQ: embedding-generation job queued
↓
Vertex AI Multimodal Embeddings API called
1408-dim vector stored in ProductEmbedding table
Product.embeddingReady = true
↓
Product now appears in:
- Web feed (immediately on publish)
- Web search (immediately)
- WhatsApp AI hybrid search (after embeddingReady = true, ~30s)
Shopper sends message/photo to WhatsApp
↓
Meta webhook → POST /whatsapp/webhook (HMAC verified)
Gemini 2.5 Flash: intent parsed, filters extracted
Text query: Vertex AI text embedding generated for the query
Image query: Cloud Vision labels extracted (context/fallback)
+ Vertex AI multimodal image embedding (primary signal)
↓
Text discovery: hybrid ranking contract
final_score = vector_similarity * 0.5 + text_rank * 0.2
+ store_performance * 0.2 + tier_boost * 0.1
Image discovery: pgvector image-embedding similarity (primary)
Tier 0 stores excluded always
↓
WhatsApp List Message + canonical product links
/stores/{storeHandle}/p/{productCode}
Shopper selects product (list row, number, or title — Redis recent results)
Shopper completes checkout on the web product page
↓
Shopper pays via Paystack
Paystack webhook → POST /payments/webhook (HMAC verified)
Order status: PENDING_PAYMENT → PAID
LedgerEntry: PAYMENT_IN logged
Escrow holds full amount
↓
BullMQ: auto-confirm warning job (DISPATCHED + 36hrs)
BullMQ: auto-confirm job (DISPATCHED + 48hrs)
WhatsApp template: order_confirmed sent to shopper
Store notified: new order (in-app + email)
↓
Store marks ready for pickup → Twizrr books delivery → Order: DISPATCHED
Shopper: WhatsApp template order_dispatched
↓
Shopper taps [I received it] OR enters 6-digit code
Order: DISPATCHED → DELIVERED → COMPLETED
LedgerEntry: ESCROW_RELEASE logged
↓
BullMQ: payout job queued
Paystack Transfer API → store bank account
LedgerEntry: PAYOUT logged
WhatsApp template: payment_received sent to store
Client uploads image to POST /upload
↓
Cloud Vision SafeSearch API called FIRST
↓
BLOCKED → reject (400), log to ModerationLog, return error to client
SENSITIVE → upload to Cloudinary with sensitive flag,
return URL with moderationStatus: SENSITIVE
SAFE → upload to Cloudinary normally,
return URL with moderationStatus: SAFE
↓
Client receives Cloudinary URL
Uses URL in product/post creation payload
Shopper buys from digital store (@yabafashion) → pays ₦40,000
↓
System creates 2 linked Orders:
Order A: shopper ↔ @yabafashion (customer-facing, ₦40,000)
Order B: @yabafashion ↔ @balogunstore (fulfillment, ₦28,000)
↓
@balogunstore notified: "New fulfillment order — prepare for pickup"
Twizrr-managed delivery picks up from @balogunstore and delivers to shopper
↓
Shopper confirms delivery
↓
BullMQ: payout job #1
Paystack Transfer → @balogunstore bank (₦28,000, no fee)
LedgerEntry: PAYOUT for physical store
BullMQ: payout job #2
Platform fee: 2% × ₦40,000 = ₦800
Digital store receives: ₦40,000 - ₦28,000 - ₦800 = ₦11,200
Paystack Transfer → @yabafashion bank (₦11,200)
LedgerEntry: PAYOUT for digital store
WHAT IT IS: WIZZA — Twizrr's AI shopping assistant on WhatsApp, not a bot
PURPOSE: help shoppers search, discover, and buy products
NOT FOR: store management, order dispatch, inventory updates
Store-management requests are redirected:
"WIZZA is for shopping. Manage your store from your twizrr store workspace."
CONVERSATION CONTEXT:
- Every session must start with consent message (NDPR compliance)
- Shopper must accept before any product search begins
- Session stored in WhatsAppSession model (linked to User via phone)
RESPONSE RULES:
- Plain text ONLY — no emojis, no markdown, no bullet points in messages
- Gemini function-calling ONLY — never free-form text responses to shoppers
- If search returns 0 results: acknowledge + suggest alternatives
- If AI fails: graceful fallback to text keyword search — never leave shopper stranded
TEMPLATE MESSAGES (6 pre-approved — submit to Meta before launch):
twizrr_onboarding_consent — first message to new shopper
twizrr_order_confirmed — after payment succeeds
twizrr_order_dispatched — after store marks dispatched
twizrr_confirm_delivery — with [Yes, I received it] button
twizrr_payment_received — to store owner on payout
twizrr_account_linked — when shopper links WhatsApp to account
TEXT DISCOVERY RANKING (implemented — approved hybrid contract):
final_score = vector_similarity * 0.5 + text_rank * 0.2
+ store_performance * 0.2 + tier_boost * 0.1
vector_similarity — pgvector cosine similarity between the Vertex AI
text embedding of the query and product embeddings
text_rank — normalized title/name/description/category match
store_performance — order completion rate normalized to [0, 1];
stores with insufficient history default to 0.5
(new eligible stores are never penalized to zero)
tier_boost — TIER_2 = 1.0, TIER_1 = 0.5
Weights are configurable via env and validated at startup:
SEARCH_WEIGHT_VECTOR=0.5
SEARCH_WEIGHT_TEXT=0.2
SEARCH_WEIGHT_PERFORMANCE=0.2
SEARCH_WEIGHT_TIER=0.1
Each weight must be numeric and >= 0; the four weights must sum to 1.0
(tiny float tolerance). Invalid weights fail startup.
IMAGE SEARCH (implemented — embedding-first with SafeSearch screening):
1. Download shopper image from Meta (never saved to disk)
2. SafeSearch screening BEFORE any discovery — block on LIKELY or
VERY_LIKELY for adult/violence/racy (same thresholds as upload
moderation; POSSIBLE and below never block; medical/spoof are
not blocking categories unless the upload policy changes).
Blocked → safe refusal copy, no labels/embedding/search/Redis
write; analytics record errorType IMAGE_BLOCKED_UNSAFE with
zeroResults false, vectorSearchUsed false, embeddingModel null.
Screening errors fail open (masked log, search continues).
3. Cloud Vision label/object/text extraction (context + fallback only)
4. Vertex AI multimodal image embedding (1408-dim) — PRIMARY signal
5. pgvector similarity retrieval over product embeddings
6. Eligibility re-validated (active/public/in-stock rules, Tier 0 excluded)
7. Embedding failure degrades gracefully to safe label fallback;
label failure does not block vector search
TIER 0 EXCLUSION: always filter WHERE store.tier != 'TIER_0'
No exceptions — Tier 0 stores never appear in any WIZZA result
REDIS RECENT-PRODUCT SELECTION:
The last shown results (text or image search) are cached per phone.
Shoppers can pick a result by list row tap, number ("2", "the first
one"), or product title. The cached payload holds safe public fields
only — never listingCode, sourceCode, physical/source store data,
or cost/margin fields.
ANALYTICS V2:
Every discovery interaction records honest values for:
vectorSearchUsed, embeddingModel, productsRankedCount,
productsShown/productsShownCount, zeroResults (true only when a real
search returned nothing — never for embedding/label errors), and
fallback/error state. No raw image data, secrets, or prompt values.
CANONICAL PRODUCT URL (the only public product link format):
/stores/{storeHandle}/p/{productCode}
NATIVE products: native public productCode + selling store handle
SOURCED listings: sourced listing public productCode + the
digital/reseller store handle
NEVER use as public URL tokens or in WIZZA replies:
productId, sourcedProductId, sourceCode, listingCode,
physicalProductId, physicalStoreId
(sourceCode is internal/store-owner-facing only)
If a safe canonical URL cannot be built (missing handle or productCode):
send no link — never fabricate one or fall back to internal ids.
Sourced listings appear to shoppers as normal products sold by the
digital/reseller Twizrr store. The digital store is the seller context
for display name, handle, URLs, and ranking (store performance + tier).
Source/supplier/dropship/physical-store relationships are INTERNAL:
- never expose supplier/source store handles, ids, contact, or address
- never expose cost, margin, linked fulfilment, or source inventory
- never use source/supplier/dropship/partner-store/physical-store
wording in shopper copy
- store type (DIGITAL | PHYSICAL) remains invisible to shoppers
storeName — PUBLIC store display name
storeHandle — PUBLIC username / URL handle (canonical URLs use this)
businessName — PRIVATE internal/legal/KYB/admin/bank identity;
never selected or exposed in WIZZA shopper-facing replies
Shopper-facing store display fallback:
storeName → storeHandle → "twizrr store"
main → production (twizrr.com / api.twizrr.com)
staging → pre-production testing
dev → integration (all feature PRs merge here first)
feature/* → individual features (branch from dev)
fix/* → bug fixes (branch from dev)
chore/* → config, tooling, cleanup
# 1. Always branch from dev:
git checkout dev && git pull origin dev
git checkout -b feat/product-listing-form
# 2. Build the feature
# 3. Run all 6 checks before committing:
cd apps/backend && pnpm run lint && npx tsc --noEmit && pnpm run build
cd apps/web && pnpm run lint && npx tsc --noEmit && pnpm run build
# 4. Open PR: feat/* → dev
# CI must pass, CodeRabbit reviews automatically
# Human approval required before merge
# 5. Merged to dev → deploys to staging for QA
# 6. PR: dev → main → deploys to productionmain,staging,dev— require PR, CI passing, reviewer approval, no direct pushes- Feature branches — push freely
feat(scope): new feature
fix(scope): bug fix
perf(scope): performance improvement
chore(scope): config, deps, tooling
refactor(scope): restructure, no behaviour change
docs(scope): documentation only
test(scope): tests only
Examples:
feat(product): add variant image assignment to listing form
fix(payout): handle Paystack transfer timeout correctly
chore(deps): upgrade Prisma to 5.15
// All responses (handled by ResponseTransformInterceptor):
{ success: true, data: T, message?: string }
{ success: false, message: string, code?: string }
// Common error codes:
PRICE_BELOW_FLOOR // SourcedProduct price below cost
INSUFFICIENT_STOCK // Variant out of stock
WEBHOOK_INVALID // Paystack HMAC mismatch
STORE_TYPE_MISMATCH // Wrong store type for action
TIER_REQUIREMENT_FAILED // Store doesn't meet tier requirements
OTP_INVALID // Wrong 6-digit delivery code
OTP_EXPIRED // Delivery code expired (48hr)
RATE_LIMIT_EXCEEDED // Too many requests// Always use NestJS built-in exceptions:
throw new BadRequestException({ message: 'Price below minimum', code: 'PRICE_BELOW_FLOOR' })
throw new NotFoundException({ message: 'Product not found' })
throw new UnauthorizedException({ message: 'Invalid token' })
throw new ForbiddenException({ message: 'Store owners only' })
throw new ConflictException({ message: 'Handle already taken' })
// GlobalExceptionFilter catches Prisma errors:
// P2002 (unique) → 409
// P2025 (not found) → 404
// P2003 (foreign key) → 400- Never commit secrets —
.envfiles are gitignored - All PRs require human review before merging to dev or main
- Auth, schema changes, and payment logic need explicit approval before coding agents proceed
- Never auto-merge agent-generated code that touches payments, webhooks, or auth
- Rotate keys immediately if any credential is exposed
- Paystack webhook: always verify HMAC — reject if invalid, log to AuditLog
- Rate limiting: ThrottlerGuard global (100/min) + per-endpoint limits in TWIZRR_DEVELOPMENT_RULES.md
- CORS: only allow origins in CORS_ORIGINS env var — never wildcard
*on shared environments - Security contact: aliameen@codeddevs.com
This codebase was previously the Swifta project.
Swifta was a B2B hardware marketplace that pivoted to twizrr.
WHAT THIS MEANS FOR YOU (coding agent):
- You may encounter old file names, comments, or variables referencing "Swifta"
- You may find modules for: RFQ, Quote, TradeFinancing, Supplier, hardware categories
- You may find schema models that no longer apply
- You may find route structures that differ from what AGENTS.md describes
WHAT TO DO:
- Follow AGENTS.md — not the old code
- If you see a conflict between the old code and AGENTS.md: AGENTS.md wins
- Do not extend, use, or reference any of these old modules:
rfq/, quote/, trade-financing/, supplier/
- The cleanup task document will handle removing old code formally
- For now: build new features in new modules, do not modify legacy code
PACKAGE SCOPE HISTORY:
- Old code may use @swifta/ or @hardware-os/
- All new code must use @twizrr/
- Do not rename existing references unless cleanup task says so
After reading this root AGENTS.md — read your app's AGENTS.md:
apps/backend/AGENTS.md — full NestJS module map, Prisma schema
patterns, BullMQ job details, API routes
apps/web/AGENTS.md — Next.js App Router structure, all pages,
component patterns, design token usage
| Field | Value |
|---|---|
| Company | CODEDDEVS TECHNOLOGY LTD |
| Location | Lagos, Nigeria |
| Website | codeddevs.com |
| Product | twizrr — twizrr.com |
| Social | @twizrrapp (all platforms) |
| Support email | support@twizrr.com |
| Dev email | aliameen@codeddevs.com |
Last updated: June 2026 Project: twizrr Maintained by: CODEDDEVS TECHNOLOGY LTD