A full-stack, natural language-first CRM platform built as a Turborepo monorepo. Marketers can build segments using plain English, generate personalized messages using AI, run campaigns through a BullMQ job queue, and track real-time delivery stats through a simulated message delivery service.
Xeno Mini CRM focuses on a streamlined, AI-native workflow where a marketer can go from intent to a running campaign in seconds:
-
Natural Language Segmenting: Marketer types an audience query (e.g., "high spenders in Delhi who ordered more than 3 times")
$\rightarrow$ The backend parses this into database rules (using Gemini AI with a robust local regex fallback)$\rightarrow$ Instantly previews matching customer count. - AI message copywriting: The system drafts 3 personalized message variants based on the channel and brand name.
- Queue-based sending: The campaign is triggered and enqueued using BullMQ.
-
Decoupled simulation: A separate
channel-stubservice simulates message delivery with random delays and callbacks. - Real-time analytics: A Next.js dashboard polls and visualizes campaign stats (delivered, opened, clicked, failed) in real-time as callbacks flow back.
| Layer | Choice | Description / Purpose |
|---|---|---|
| Monorepo | Turborepo | Package management and parallel build orchestration. |
| Frontend | Next.js 16 (App Router) | Modern UI styled using Tailwind CSS v4 and customized glassmorphic components. |
| CRM API | Express + TypeScript | Lightweight Node.js server handling segment rules, campaigns, and callback ingestion. |
| Channel Stub | Express + TypeScript | Decoupled message sending simulator mimicking external providers (e.g., Twilio). |
| AI Layer | Gemini API (SDK) | Natural language segment parsing and marketing copy generation (with automatic local regex/template fallbacks). |
| Database | PostgreSQL | Relational storage for customer profiles, orders, campaigns, and delivery logs. |
| Job Queue | BullMQ + Redis | High-throughput asynchronous message fan-out, retry handling, and concurrency limits. |
graph TD
subgraph CRM_Frontend ["CRM Frontend (Next.js 16)"]
UI["Next.js Web UI"]
end
subgraph CRM_API_Service ["CRM API Service (Express)"]
API["Express App (App.ts)"]
SegmentEngine["Segment SQL Engine"]
AIService["AI Service (Gemini / Fallback)"]
BullMQ_Queue["BullMQ Send Queue"]
BullMQ_Worker["BullMQ Worker (Concurrency: 5)"]
end
subgraph External_Datastores ["Infrastructure"]
Postgres[("PostgreSQL DB")]
Redis[("Redis (BullMQ & Cache)")]
end
subgraph Channel_Stub_Service ["Channel Stub (Express)"]
StubAPI["POST /send"]
Simulator["Delivery Outcome Simulator"]
end
UI <-->|REST API| API
API <-->|AI Prompts| AIService
API <-->|SQL Queries| SegmentEngine
SegmentEngine <--> Postgres
API <--> Postgres
API -->|Queue Jobs| BullMQ_Queue
BullMQ_Queue <--> Redis
BullMQ_Worker <--> Redis
BullMQ_Worker -->|POST /send| StubAPI
StubAPI --> Simulator
Simulator -->|Async Callback POST /receipt| API
customers
βββ id (UUID, PK)
βββ name (VARCHAR)
βββ email (VARCHAR, UNIQUE)
βββ phone (VARCHAR)
βββ city (VARCHAR)
βββ created_at (TIMESTAMP)
orders
βββ id (UUID, PK)
βββ customer_id (UUID, FK -> customers.id)
βββ total_amount (DECIMAL)
βββ ordered_at (TIMESTAMP)
βββ status (VARCHAR)
campaigns
βββ id (UUID, PK)
βββ name (VARCHAR)
βββ channel (VARCHAR: email | sms | whatsapp | rcs)
βββ status (VARCHAR: draft | sending | sent)
βββ message_body (TEXT)
βββ scheduled_at (TIMESTAMP)
βββ sent_at (TIMESTAMP)
βββ created_at (TIMESTAMP)
segments
βββ id (UUID, PK)
βββ campaign_id (UUID, FK -> campaigns.id)
βββ name (VARCHAR)
βββ rules (JSONB: [{field, operator, value}])
βββ matched_count (INT)
campaign_recipients
βββ id (UUID, PK)
βββ campaign_id (UUID, FK -> campaigns.id)
βββ customer_id (UUID, FK -> customers.id)
βββ status (VARCHAR: queued | sent | delivered | opened | clicked | failed)
βββ message_body (TEXT)
βββ sent_at (TIMESTAMP)
delivery_events
βββ id (UUID, PK)
βββ recipient_id (UUID, FK -> campaign_recipients.id)
βββ event_type (VARCHAR: sent | delivered | opened | clicked | failed)
βββ metadata (JSONB)
βββ occurred_at (TIMESTAMP)
xeno-crm/
βββ apps/
β βββ crm-frontend/ # Next.js 16 Frontend App
β β βββ src/app/ # Page routes (Dashboard, Customers, Campaigns)
β β βββ src/components/ # Reusable UI components (Sidebar, SegmentBuilder, etc.)
β β
β βββ crm-api/ # Express CRM API (Backend)
β β βββ src/db/ # Database config, migration & seed scripts
β β βββ src/routes/ # API endpoints (customers, campaigns, receipts)
β β βββ src/services/ # SQL Segment Engine, BullMQ worker & AI Service
β β
β βββ channel-stub/ # Express delivery simulator
β βββ src/ # Simulator & Callback dispatcher
β
βββ packages/
β βββ shared-types/ # Shared TypeScript interfaces (build-time package)
β
βββ docker-compose.yml # Postgres + Redis dev containers
βββ turbo.json # Turborepo configurations
βββ package.json # Monorepo root dependencies & scripts
- Node.js (v18 or higher recommended)
- Docker & Docker Compose (for Postgres & Redis)
Install all workspace dependencies from the root directory:
npm installCreate .env files for the backend services:
-
CRM API: Create
apps/crm-api/.envDATABASE_URL=postgresql://xeno:xeno_secret@localhost:5432/xeno_crm REDIS_URL=redis://localhost:6379 CHANNEL_STUB_URL=http://localhost:3002 PORT=3001 CRM_API_URL=http://localhost:3001 GEMINI_API_KEY=your_gemini_api_key_here
-
Channel Stub: Create
apps/channel-stub/.envPORT=3002 CRM_API_URL=http://localhost:3001
-
CRM Frontend: Create
apps/crm-frontend/.env.localNEXT_PUBLIC_API_URL=http://localhost:3001
Start PostgreSQL and Redis in the background using Docker Compose:
docker compose up -dCreate the database tables and populate them with mock customers and orders (200 customers, 500+ orders spread across different cities and purchase timelines):
# Run migrations
npm run db:migrate
# Seed database
npm run db:seedRun all services simultaneously in development mode:
npm run dev- Frontend: http://localhost:3000
- CRM API: http://localhost:3001
- Channel Stub: http://localhost:3002
Ensure you set the Root Directory setting to empty (the repo root) and configure:
- crm-api:
- Build Command:
npm install && npx turbo run build --filter=crm-api... - Start Command:
node apps/crm-api/dist/app.js - Add the
.envvariables in Render's Environment settings. - To run migrations automatically on build, set the build command to:
npm install && npx turbo run build --filter=crm-api... && cd apps/crm-api && npx tsx src/db/migrate.ts
- Build Command:
- channel-stub:
- Build Command:
npm install && npx turbo run build --filter=channel-stub... - Start Command:
node apps/channel-stub/dist/app.js
- Build Command:
- Vercel will automatically detect the Next.js app.
- Set the Root Directory to
apps/crm-frontend. - Add
NEXT_PUBLIC_API_URLpointing to your deployed Rendercrm-apiURL.
The segmentEngine.ts maps AI-generated segment rules into parameterised SQL clauses. Table field names are checked against an explicit whitelist (total_spend, order_count, days_since_last_order, city), and user inputs are bound safely to PostgreSQL placeholders ($1, $2, etc.), preventing raw query manipulation.
Campaign sends are queued immediately, returning a 202 Accepted to the client. BullMQ processes these jobs asynchronously. The sendWorker runs with a concurrency limit of 5 to protect database connection pools and match downstream API rate limits.
Because network channels are unreliable, the channel-stub retries delivery callbacks if the CRM endpoint is offline. To prevent duplicate stats tracking, delivery_events has a unique compound key constraint: UNIQUE(recipient_id, event_type). Any duplicate callbacks sent during retries are safely ignored by the database.
In case of Gemini API rate-limits (429 Quota Exceeded) or network downtime, the app falls back automatically to:
- A regex-based parser that interprets cities, spends, order counts, and inactivity limits locally.
- A local template-based copy generator that builds customized drafts based on channel types.