Skip to content

Latest commit

 

History

History
1298 lines (1074 loc) · 56.9 KB

File metadata and controls

1298 lines (1074 loc) · 56.9 KB

twizrr — AI Assistant Instructions

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.


0. Project Overview

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

1. What This Project Is

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.

2. Tech Stack

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
Email 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)

3. Design System

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.

Brand Colors

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)

Typography

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

Design Direction

  • 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

Logo

  • 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

4. Folder Structure

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

Architecture Rationale

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.

5. Database Schema

Full schema is defined in apps/backend/prisma/schema.prisma. Full model documentation is in TWIZRR_DATA_ARCHITECTURE.md. Below are the critical models and rules every agent must know.

Critical Schema Rules

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

Key Models (summary)

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

6. Environment Variables

# 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)

7. API Routes

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" }

Route Prefix Groups

/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)

8. Route Protection

// 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 access

9. MVP Scope

Full MVP specification is in TWIZRR_MVP_SCOPE.md. Everything in that document must work before launch. Below is the quick reference for agents.

Build in MVP (do build these):

  • 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

Do NOT build in MVP (these are deferred):

  • 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

10. Business Rules

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

11. Naming Conventions

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)

12. Coding Rules

  1. TypeScript strict mode always — no any types. Use unknown with type guards when needed.

  2. Money is always BigInt — never Float, Decimal, or Number for monetary values. Store in kobo. Display as Naira in frontend only.

  3. No console.log in production code — backend: use NestJS Logger (pino). Frontend: if (process.env.NODE_ENV === 'development') guard.

  4. Validate all input — backend: class-validator + class-transformer on every DTO. Frontend: Zod schemas. Never trust client input.

  5. pnpm only — never use npm or yarn. If you see a package-lock.json or yarn.lock — delete it.

  6. Auth check first — on every protected endpoint, verify JWT before any business logic runs.

  7. Paystack HMAC before any payment processing — verify signature on every webhook call. Reject and log if invalid.

  8. Cloud Vision before Cloudinary — every image upload must be moderated first. Never upload to Cloudinary without a SAFE or SENSITIVE result from Cloud Vision.

  9. 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.

  10. All API responses use the envelope format:

    { "success": true, "data": {}, "message": "optional" }
    { "success": false, "message": "error description", "code": "ERROR_CODE" }

    The ResponseTransformInterceptor handles this automatically. Use @Res() only when you need to bypass it (e.g. USSD callbacks, WhatsApp webhook verification).

  11. BullMQ for all async jobs — never do payouts, notifications, or embeddings synchronously. Always queue them.

  12. Idempotency keys on payments — every Payment and Payout record must have an idempotency key. Check before processing to prevent duplicates.

  13. Use design tokens, not hardcoded values — frontend: CSS variables only. Never hardcode hex colours, font sizes, or spacing values.

  14. Mobile-first CSS — all layouts start at 375px. Use sm:, md:, lg: Tailwind breakpoints for wider layouts.

  15. No emojis anywhere — icons only in UI (Lucide React). Plain text in WhatsApp messages. Zero tolerance.

  16. 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.

  17. 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.

  18. 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.

  19. Never push directly to main or dev — always use feature branches. PRs only.

  20. 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.


13. NestJS — Critical Patterns

// 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)

14. BullMQ Queues

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

15. Data Flows

Product Listed → Searchable on WhatsApp

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 Buys via WhatsApp

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

Image Upload (Product Photo or Post Image)

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

Dropship Order → Two Payouts

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

16. WhatsApp AI (WIZZA) Rules

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

Current WhatsApp/WIZZA Discovery Architecture

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.

Public Product Identity and URL Model

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 Listing Shopper-Visibility Model

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

Store Identity Privacy

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"

17. GitHub Workflow

Branch structure

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

How to contribute

# 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 production

Branch protection

  • main, staging, dev — require PR, CI passing, reviewer approval, no direct pushes
  • Feature branches — push freely

Commit message format (Conventional Commits)

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

18. Error Handling

Response envelope

// 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

NestJS exception pattern

// 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

19. Security

  • Never commit secrets — .env files 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

20. Codebase History (Important)

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

21. Per-App Instructions

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

22. Company Details

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