Note: EcomPilot CRM is an internal product I designed, built and operate for an e-commerce services business. It runs in production, is login-gated, and its source is private. This repository contains architectural documentation, design decisions, and verified numbers from the codebase — no product code and no client data.
- Overview
- What the Platform Does
- Architecture at a Glance
- Technology Stack
- Technical Highlights
- Security & Audits
- By the Numbers
- Known Limitations & Trade-offs
- Additional Documentation
- License
- Contact
A full-stack CRM + operations platform for an e-commerce services company selling on Allegro and other marketplaces. It replaced a patchwork of HubSpot, spreadsheets and chat threads with one system: sales pipeline, tasks, calendar, finance, offer/contract generation — fed by six integration sources and exposed to AI agents through a 30-tool MCP server.
Three properties distinguish it from a typical internal CRM:
- It's AI-native, not AI-bolted-on. The same REST API that serves the React frontend is exposed to Claude via a Model Context Protocol server (30 read/write tools), and an in-app assistant answers questions from real database queries and RAG over notes — with a propose→execute loop for write actions.
- It's built for unattended operation. Every integration reconnects and re-syncs on its own: the calendar push queue has a dead-letter state machine, lead ingestion is idempotent under webhook retry storms, and each external dependency degrades to a clean no-op when unavailable.
- I operate it, not just ship it. Backups, deploys, audits and incident handling are mine. Several design decisions below only make sense once you know somebody has to run this thing alone.
Role: Designer / Developer / Operator (solo) Status: Production (daily business use) Stack: FastAPI + React 19 + PostgreSQL 16, self-hosted on a European VPS
| Area | Capabilities |
|---|---|
| Sales pipeline | Configurable stages, drag-and-drop Kanban board, stage history, closed/archived views, contract-sign flow, marketplace badges |
| Leads | Full activity timeline (notes, calls, meetings, emails, WhatsApp), file attachments, per-lead platform-user mapping |
| Tasks & calendar | Custom-built time-grid calendar with drag, resize and reschedule-drop; month/week/day views; two-way Google Calendar sync; auto-tasks |
| Finance | Invoices, expenses, cash-flow forecasting, invoice status tracking, per-rep performance |
| Documents | Offer PDFs (Jinja2 + WeasyPrint) plus invoice and contract PDFs (fpdf2), all generated server-side, with per-marketplace pricing |
| Inbox & notifications | Email templates, outbound email, team bell notifications on new leads |
| AI assistant | Grounded Q&A over CRM data with citations, structured analytics answers, propose→execute write actions |
| MCP server | 30 tools exposing the live app to Claude for read and write — see MCP_SERVER.md |
| Audit journal | In-transaction audit logging surfaced as a browsable journal page |
Six integration sources feed the system: Meta Lead Ads, Fathom (meeting notetaker), Google Calendar (two-way), WhatsApp (group archive), email, and a one-time HubSpot migration — orchestrated through n8n. Details in INTEGRATIONS.md.
graph TB
subgraph "External Sources"
META[Meta Lead Ads]
FATHOM[Fathom Notetaker]
GCAL[Google Calendar]
WA[WhatsApp Archive]
MAIL[Email]
PLATFORM[Parent Platform API]
end
subgraph "Integration Layer"
N8N[n8n Orchestrator<br/>webhooks in & out]
end
subgraph "AI Clients"
CLAUDE[Claude Desktop]
MCP[MCP Server<br/>30 tools, stdio]
end
subgraph "Application"
SPA[React 19 SPA<br/>Vite + TypeScript]
API[FastAPI Backend<br/>25 REST modules]
SCHED1[Autosync Scheduler<br/>platform order sync]
SCHED2[Calendar Reconciler<br/>push queue + dead-letter]
ASSIST[AI Assistant<br/>structured + RAG]
end
subgraph "Data Layer"
PG[(PostgreSQL 16<br/>38 Alembic migrations)]
UPLOADS[/uploads volume/]
end
META --> N8N
GCAL <--> N8N
MAIL <--> N8N
WA --> N8N
N8N -->|signed webhooks| API
FATHOM -->|HMAC-verified webhook| API
PLATFORM <-->|polled REST| SCHED1
CLAUDE <--> MCP
MCP -->|authenticated REST| API
SPA --> API
API --> PG
API --> UPLOADS
SCHED1 --> API
SCHED2 -->|outbound pushes| N8N
ASSIST --> PG
API --> ASSIST
Everything converges on one REST API: the browser, the MCP server, the schedulers and the webhook sources are all just clients of the same FastAPI surface. See ARCHITECTURE.md for the full breakdown.
- Framework: FastAPI (Python 3.11,
uvfor dependency management) - Database: PostgreSQL 16, SQLAlchemy models, Alembic migrations (38)
- Auth: JWT bearer (HS256), bcrypt hashing, token invalidation on password change (
iatvspassword_changed_at) - PDF generation: server-side — Jinja2 + WeasyPrint for offers, fpdf2 for invoices and contracts
- Background work: two in-process schedulers (platform order autosync, calendar push reconciler) — no Celery, by design at this scale
- Config discipline: production refuses to boot with default secrets, placeholder tokens or a localhost database
- MCP server: FastMCP over stdio, 30 read/write tools, lazy login as a real CRM user
- LLM: Anthropic Claude (Messages API) for chat/assistant; pluggable providers
- Embeddings: Voyage (
voyage-3) by default, OpenAI supported; vectors stored as float arrays in PostgreSQL, cosine similarity computed in Python — a deliberate no-pgvector trade-off, see Highlights - Grounding: strict citation-based answers; structured analytics path answers finance/pipeline questions from real SQL, not embeddings
- Framework: React 19 + TypeScript + Vite
- State: React Context (auth, team, toasts) + a hand-rolled typed API client — no Redux/React-Query
- Custom UI: time-grid calendar with drag/resize/reschedule and a drag-and-drop pipeline board, both built from scratch
- Testing: Vitest, 142 test files
- Deploy: Docker Compose — PostgreSQL + backend + nginx, private network, only the reverse proxy published; host-level TLS termination
- Backups: nightly
pg_dump | gzipshipped offsite + pre-deploy snapshots; documented disaster-recovery runbook - Health: container healthchecks + an autosync health snapshot exposed via the API; calendar sync keeps its own state endpoint
Problem: outbound calendar sync goes through a webhook broker (n8n) that can be down, slow, or mid-redeploy at any moment. Fire-and-forget pushes silently lose meetings — the failure mode users notice a week later, in front of a client.
Solution: the push signal is committed with the data, then reconciled asynchronously. Ordinary changes flip a sync_status flag on the entity row itself — set in the same transaction, so the reconciler can't miss what the database committed. Changes that would erase their own signal (task deletes and moves, where the row disappears or changes identity) get a dedicated pending_calendar_push queue row instead, also written in-transaction. A reconciler daemon drives the queue through pending → pushing → done, with retry up to 5 attempts and dead-lettering after that (kept, inspectable, not retried forever). Queue rows stuck in pushing (process died mid-push) are reclaimed at boot. Recurring-event instance IDs get dedicated handling so a recurring series doesn't fan out into duplicate pushes.
The result is that a broker outage degrades to "meetings sync a few minutes late" instead of "meetings silently vanish". See INTEGRATIONS.md → Google Calendar.
Problem: Meta Lead Ads webhooks (via n8n) retry aggressively on timeouts — the same lead can arrive three times within seconds, interleaved. Naive handling creates duplicate leads; naive locking deadlocks the request pool.
Solution: concurrent creations for the same contact are serialized with a PostgreSQL advisory lock keyed on the normalized email (pg_advisory_xact_lock(hashtext(email))), so interleaved retries queue up behind each other and the dedup check inside the lock sees the winner's row. Records the integrations create downstream — auto-tasks and imported activities — carry DB-level unique ingestion keys as the final backstop, so a replay collapses instead of duplicating.
Problem: exposing a live business system to an LLM usually means building a parallel integration surface that drifts from the real app.
Solution: the MCP server is a ~1,000-line thin client over the same REST API the frontend uses. It logs in lazily as a real CRM user, caches the bearer token, retries once on 401 — and every write lands in the same audit log as a human's click. 30 tools (16 read, 14 write) cover pipeline, leads, tasks, finance and email. No parallel data path, no schema drift, nothing to keep in sync.
See MCP_SERVER.md for the full tool catalog and design notes.
Problem: RAG over notes gives fluent but approximate answers to questions like "what's our invoice total this month?" — which have exact answers in the database.
Solution: incoming questions are classified. Analytics questions (finance, pipeline, rep performance) route to a structured-answer path that runs real database queries and formats the result. Everything else falls back to RAG over activities, notes, WhatsApp messages and calendar events, with strict citation-based grounding — the model may only answer from retrieved context and must cite sources. Write-intent messages become proposed actions the user explicitly confirms before execution.
Problem: the fashionable default (pgvector + ANN index) adds an extension dependency, migration complexity and operational surface for a corpus that is — realistically — thousands of chunks, not millions.
Solution: embeddings are stored as plain float arrays in a regular table; cosine similarity is computed in Python at query time. The index tops itself up automatically before retrieval, so answers see fresh data without a manual reindex step. At this scale the scan is milliseconds, the whole feature is one table plus two services, and there is nothing extra to operate. The honest boundary: past ~10⁵ chunks this needs pgvector — tracked as the known upgrade path, in Known Limitations.
Problem: a solo-operated system accumulates integrations faster than it accumulates operators. Each one is a potential boot failure, test-suite dependency, and 3am page.
Solution: every optional subsystem — embeddings, chat LLM, calendar push, notetaker webhook — checks its own config key and cleanly no-ops when unset (API endpoints return an explicit "not configured" status rather than erroring). Services are kept import-light so tests monkeypatch them cheaply. This is a big part of why the test suite could grow to 1,647 backend test functions: nothing external is required to run any of it.
Problem: the most dangerous deploy is the one where a default secret or placeholder token silently ships.
Solution: startup validation hard-fails production boots on default JWT secrets, short secrets, localhost database URLs and placeholder webhook tokens. The system prefers not starting to starting wrong — the operational corollary of "everything fails, mine gets back up" is "some things should refuse to get up".
Security work on the system is continuous and documented internally:
- Adversarial code audit — a full manual review produced 37 confirmed findings across severity levels; fixes shipped in three waves, with finding IDs referenced directly in code comments and config validation so every fix traces back to its finding.
- Automated scanning — dependency audits (npm audit, pip-audit) and an OWASP ZAP baseline scan run against the deployed application, feeding the same fix-tracking process.
- Auth hardening — JWT with password-change invalidation, bcrypt, login rate limiting with bounded memory, and production config guards (see Highlight 7).
- In-transaction audit logging — every meaningful write (human or MCP) lands in an audit journal, browsable in the app.
Specific findings, dependency versions and remediation details stay internal — publishing a vulnerability map of a reachable production system would be the opposite of security work.
All numbers below are computed from the codebase (counted, not estimated):
| Metric | Value |
|---|---|
| Backend test functions | 1,647 in 101 test files |
| Frontend test files | 142 (Vitest) |
| MCP tools | 30 (16 read, 14 write) |
| REST API modules | 25 |
| Alembic migrations | 38 |
| SQLAlchemy models | 19 |
| Backend services | 24 |
| Frontend pages | 15 |
| Backend code | ~16k lines of Python |
| Frontend code | ~21k lines of TypeScript |
| MCP server | ~1k lines of Python |
No uptime percentages or latency numbers are quoted because I don't publish figures I can't back with reproducible measurements.
Honest list of things I'd flag to a reviewer:
- In-process schedulers assume a single worker. The autosync and calendar-reconcile daemons run as threads inside the API process. Scaling to multiple workers needs a PostgreSQL advisory lock around each cycle — documented in the code, not yet needed at current load.
- No end-to-end tests. The 1,647 + 142 tests are unit/integration level. The custom calendar's drag/resize interactions are exactly the kind of thing a Playwright suite would cover; it's the first thing I'd add with more capacity.
- RAG does brute-force cosine in Python. Right for thousands of chunks, wrong for millions — pgvector is the known upgrade path when the corpus outgrows the current approach.
- The frontend data layer is hand-rolled. A typed fetch client with no cache/invalidation layer. It's small and predictable, but React Query would remove a class of manual refetch bookkeeping as the app grows.
- MCP actions are attributed to the service user. Writes made through Claude land in the audit log under the MCP user's identity, not the human operating Claude. Fine for a solo operator; would need per-operator tokens before a team uses it.
- Authorization is intentionally coarse. Roles exist in the schema, but for a small trusted team most endpoints require only an authenticated active user. Fine-grained RBAC is the tracked next step if the team grows.
- n8n is a single point of failure for four integrations. By design (one place to operate), and failures degrade to queued-and-retried rather than lost — but a broker outage pauses calendar/email/Meta/WhatsApp flows until it's back.
- Monitoring is healthchecks + logs. There's a scheduler health snapshot in the API and container healthchecks, but no APM or error-tracking service on this system yet.
- ARCHITECTURE.md — backend/frontend structure, request lifecycle, schedulers, deployment, backups
- MCP_SERVER.md — the 30-tool MCP server: auth model, tool catalog, design decisions
- INTEGRATIONS.md — all six sources, the n8n hub, idempotency and the calendar push queue
Documentation in this repository is released under Creative Commons Attribution-NonCommercial 4.0 (CC BY-NC 4.0). The product's source code is proprietary and not included here.
Open to full-stack / systems work (remote, B2B preferred).
- Web: paradoxlab.dev
- GitHub: github.com/paradoxlabdev
Last updated: July 2026