Built for the Xeno engineering take-home. Reach lets a beauty brand's marketer describe a goal in plain English and have an AI copilot turn it into a launched, tracked campaign — sizing the audience, drafting the copy, picking the channel, and surfacing live performance.
Brand persona: SUGAR Cosmetics (a D2C beauty brand). All data is simulated; this is an unofficial demo and is not affiliated with SUGAR Cosmetics.
- Live app: https://reach-crm.vercel.app
- CRM API: https://reach-crm.onrender.com · Channel service: https://reach-channel.onrender.com
- Walkthrough video:
⏳ The backend runs on Render's free tier, which sleeps after ~15 min idle — the first request may take ~50s to wake. Just load the app once and give it a moment.
| AI Copilot (chat-first) | Live campaign funnel |
|---|---|
![]() |
![]() |
The brief is deliberately open, so the sharpest thing to do is commit to one point of view. Reach's bet: the marketer talks to an agent, not a form.
"Win back customers who bought lipstick but haven't ordered in 60 days."
The agent (a provider-agnostic LLM tool-use loop — runs on Groq Llama 3.3 70B or Google Gemini 2.5 Flash) responds by actually doing the work:
- Ingest — customers + orders come in via a seed of realistic SUGAR data and a runtime
ingest API (
POST /ingest) with a CSV/JSON upload on the Customers page. - Sizes the audience — translates the goal into a structured filter and previews the count.
- Drafts the copy — on-brand, personalized (
{{name}},{{city}},{{last_item}}). - Recommends a channel — WhatsApp / SMS / Email / RCS, with a reason.
- Stages a campaign as a draft — and hands control back to the human.
- The marketer clicks Approve & Launch; from there Reach tracks the full delivery + engagement funnel live.
A classic dashboard (Campaigns, Segments, Customers) sits alongside the chat so nothing is a black box — every audience and campaign the agent creates is inspectable.
What I deliberately did not build: sales/pipeline/leads/tickets, real messaging providers, auth/multi-tenancy, A/B testing, migrations tooling. Each is a conscious cut to keep the surface sharp (see §7).
┌─────────────────────────────────────────────┐
│ Browser (React) │
│ AI Copilot · Campaigns · Segments · Cust. │
└───────────────┬─────────────────────────────┘
│ REST + polling
▼
┌──────────────────────────────────────────────────────────────┐
│ CRM service (FastAPI) │
│ │
│ /agent/chat ── Gemini tool-use loop ── tools: │
│ preview_audience · create_segment · stage_campaign │
│ get_campaign_performance │
│ │
│ Segmentation: Filter DSL ──► SQLAlchemy query │
│ Launch: materialise Communications ──► dispatch ──────┼──┐
│ /webhooks/receipts ◄── idempotent, order-independent ingest │ │ HMAC-signed
│ Analytics: funnel + attributed revenue │ │ send batch
└───────────────────────────┬────────────────────────────────────┘ │
│ Postgres (prod) / SQLite (local) │
▼ ▼
┌────────────────┐ ┌──────────────────────────────────┐
│ Postgres │ │ Channel service (FastAPI) │
└────────────────┘ │ │
│ intake queue → worker pool │
HMAC-signed callbacks ◄──────────────────┤ probabilistic lifecycle sim │
(delivered/opened/read/ │ out-of-order + duplicate events │
clicked/converted/failed) │ retry w/ exponential backoff │
└──────────────────────────────────┘
Two independently deployable services + a frontend, in one repo:
| Path | What it is |
|---|---|
crm/ |
The product. Ingestion, segmentation, the agent, launch, webhook ingest, analytics. |
channel-service/ |
A stub of a messaging provider. Delivers nothing real — it simulates the lifecycle and calls back. |
web/ |
React + Vite + TypeScript dashboard & chat. |
These are the most logic-dense, most-defensible parts of the codebase.
The agent never writes SQL. It emits a small declarative filter that Pydantic validates against a whitelist of fields and operators, which is then compiled to a parameterised SQLAlchemy query:
{ "match": "all",
"conditions": [
{ "field": "last_order_days_ago", "op": "gt", "value": 60 },
{ "field": "category", "op": "in", "value": ["lipstick"] } ] }Why this shape: no injection, no hallucinated columns, and portability — relative-date conditions ("ordered > 60 days ago") are translated into absolute timestamp cutoffs in Python, so the identical DSL runs on SQLite (local/tests) and Postgres (prod) with no dialect-specific date math.
crm/app/services/receipts.py · crm/app/models.py
A communication's status is derived from which lifecycle timestamps are set
(converted > clicked > read > opened > delivered > sent), not stored as mutable state.
Combined with a UNIQUE event_id, this gives three properties the brief explicitly probes:
- Exactly-once — a retried/duplicate callback is recorded once; the rest are no-ops.
- Order-independent — a
readarriving beforedeliveredstill yields a correct funnel. - No double-counted revenue — conversion value is attributed once, guarded by the same dedupe.
The channel service deliberately injects ~5% duplicate callbacks and dispatches every
event on an independent jittered timer (so callbacks arrive out of order) precisely to
exercise these guarantees. Covered by crm/tests/test_receipts.py.
channel-service/app/worker.py · simulator.py
The send/receipt loop is modelled close to how real channel delivery works:
- CRM
POST /v1/sendwith an HMAC-signed batch → channel service returns202immediately. - A bounded intake queue + worker pool drains the batch; each communication's lifecycle runs as its own task. The queue is the backpressure point.
- A separate semaphore caps concurrent outbound callbacks — the genuinely constrained resource (HTTP connections to the CRM) — so a worker is never held hostage for a message's multi-second lifecycle. Intake therefore stays fast under load.
- Each event posts back to
/webhooks/receiptswith HMAC + retry/exponential backoff, so a transiently-down CRM doesn't lose events.
This separation — intake concurrency vs outbound rate — is the deliberate design choice. At scale the queue becomes SQS/Kafka and the workers a horizontally-scaled consumer group; the shape is unchanged (see §7).
- Agent loop:
crm/app/llm/agent.pyruns a bounded (MAX_STEPS) model→tools→model loop. Tools are the only way the model touches the CRM (llm/tools.py). - Provider-agnostic:
crm/app/llm/provider.pydefines a normalized interface with two implementations —GroqProvider(Llama 3.3 70B) andGeminiProvider(Gemini 2.5 Flash) — selected by theLLM_PROVIDERenv var. The agent loop is identical for both; the live demo runs on Groq. Adding OpenAI/etc. is one more class. - Human-in-the-loop by construction: there is no
launchtool. The agent can only stage adraft; a human must approve the send in the UI. Safety isn't a prompt, it's the API surface. - Graceful degradation: if the active provider has no key, the chat disables itself and the
whole dashboard still works — a free-tier rate limit can't break a demo. (This swappability
earned its keep: when Gemini's free tier blocked generation for our project, flipping
LLM_PROVIDER=groqwas the only change needed.)
AI-native workflow: this project itself was built with an AI coding agent — used to scaffold the services, generate the Filter-DSL compiler and the idempotent receipt handler, and write the test suite — with each piece reviewed, run, and corrected against real output (e.g. the worker was refactored after the first version blocked throughput). See the video.
Prerequisites: Python 3.12, Node 20+. (Docker optional.)
# 1) Channel service (terminal A)
cd channel-service && python -m venv .venv && .venv/Scripts/pip install -r requirements.txt
.venv/Scripts/uvicorn app.main:app --port 8001
# 2) CRM (terminal B) — defaults to SQLite, no DB setup needed
cd crm && python -m venv .venv && .venv/Scripts/pip install -r requirements.txt
copy .env.example .env # then add an LLM key for the chat (see below)
.venv/Scripts/python -m app.seed # ~1,000 customers + ~5,000 orders
.venv/Scripts/uvicorn app.main:app --port 8000
# 3) Frontend (terminal C)
cd web && npm install && npm run dev # http://localhost:5173LLM key (for the chat — the dashboard works without it). Pick one in crm/.env:
- Groq (used in the live demo):
LLM_PROVIDER=groq,GROQ_API_KEY=...— free key at https://console.groq.com/keys - Gemini:
LLM_PROVIDER=gemini,GEMINI_API_KEY=...— free key at https://aistudio.google.com/apikey
Or with Docker (Postgres + both services): docker compose up --build, then
docker compose exec crm python -m app.seed.
Tests: cd crm && .venv/Scripts/python -m pytest (DSL translation + webhook idempotency).
| Decision (this scope) | At real scale |
|---|---|
| In-process asyncio queue + worker pool | SQS/Kafka + a horizontally-scaled consumer group |
| Polling for live UI | SSE / WebSockets pushed from the CRM |
create_all schema |
Alembic migrations |
| SQLite local / single Postgres prod | Postgres with read replicas; partition communications |
| Synchronous launch (materialise + POST) | Background job; chunked dispatch with rate limits |
| Whole-history-per-turn agent context | Summarised memory + retrieval over past campaigns |
| Single shared HMAC secret | Per-tenant keys + rotation |
Assumed scale for this build: thousands of customers, campaigns of a few hundred–thousand recipients, callbacks playing out in seconds. The code is structured so the shapes (queue, idempotent ingest, derived state) survive the jump; the infrastructure is what changes.
Backend: FastAPI · SQLAlchemy 2 (async) · Pydantic · httpx · Postgres/SQLite · LLM: Groq (Llama 3.3 70B) or Google Gemini 2.5 Flash, behind one swappable interface · pytest. Frontend: React 18 · Vite · TypeScript · Recharts. Deploy: Vercel (web) · Render (both services + Postgres).

