Local-first AI e-commerce analytics. Your data never leaves your machine.
git clone https://github.com/karankashyap/cartograph && cd cartograph
docker compose -f deploy/docker-compose.yml up
# open http://localhost:3000
# drag sample-data/shopify_orders.csv onto the import panelCartograph_demo.mp4
- Analytics dashboard — revenue, AOV, cohort retention, dead stock, inventory velocity
- AI narrative — grounded insights, zero hallucinated numbers (V3: only pre-computed metrics passed to LLM)
- Text-to-SQL chat — multi-turn conversation history, safe read-only queries, 6-layer guardrail suite
- AI provider switching — Ollama (fully local) or LM Studio (Gemma 4 / any OpenAI-compatible server)
- Content studio — product descriptions, SEO copy, email campaigns
- Semantic product search — pgvector + HNSW index via
nomic-embed-textembeddings - Expo mobile dashboard — React Native metrics + charts
- Multi-platform import — Shopify, Amazon, WooCommerce CSV
graph TD
Browser["Browser :3000"] -->|GraphQL / WS| API["Go API :8080\ngqlgen + pgx"]
Mobile["Expo Mobile"] -->|GraphQL| API
API -->|SQL| DB["Postgres 16\n+ pgvector"]
API -->|HTTP| Ollama["Ollama :11434\nllama3.2 (default)"]
API -->|HTTP| LMStudio["LM Studio :1234\nGemma 4 / any model"]
Worker["Go Worker"] -->|embed jobs| DB
Worker -->|embed model| Ollama
CSV["CSV Upload\nShopify / Amazon / Woo"] -->|parse + upsert| Worker
| Layer | Tech |
|---|---|
| API | Go 1.22 · gqlgen · pgx/v5 |
| LLM | Ollama (llama3.2) · LM Studio (Gemma 4) — fully local, OpenAI-compatible |
| Database | Postgres 16 + pgvector + pg_trgm |
| Web | Next.js 14 (App Router) · Tailwind · urql |
| Mobile | Expo SDK 56 · victory-native |
| Monorepo | Turborepo + pnpm workspaces |
| Infra | Docker Compose — single up to run everything |
Cartograph supports two local LLM providers, switchable per-request from the UI dropdown.
| Provider | Default model | Use case |
|---|---|---|
| Ollama | llama3.2 |
Fast, lightweight, runs anywhere |
| LM Studio | gemma-4-27b |
Higher accuracy, requires GPU |
Override via environment variables in deploy/.env:
OLLAMA_URL=http://host.docker.internal:11434
OLLAMA_MODEL=llama3.2
LM_STUDIO_URL=http://host.docker.internal:1234
LM_STUDIO_MODEL=gemma-4-27bLM Studio must be set to listen on
0.0.0.0(not127.0.0.1) for Docker to reach it viahost.docker.internal.
LLM proposes, deterministic Go disposes. Store isolation is enforced server-side — the LLM never sees or sets store_id.
Layer 1: keyword blocklist — rejects INSERT/UPDATE/DELETE/DROP/…
Layer 2: statement type check — rejects anything that is not SELECT or WITH
Layer 3: read-only Postgres role — cartograph_chat user: SELECT only, no writes physically possible
Layer 4: store filter injection — server rewrites FROM/JOIN to filtered subquery before execution
Layer 5: LIMIT injection — caps result set at 100 rows
Layer 6: repair loop — on exec error, sends failed SQL + Postgres error back to LLM for one retry
Full test suite: 20+ malicious inputs, all blocked. See services/api/internal/ai/sql_test.go.
V1: ∀ SQL from LLM → guardrail.Validate() before execution
V2: text-to-SQL role = cartograph_chat (read-only, no writes physically possible)
V3: narrative → metrics JSON only passed, no raw rows
V4: email → SHA-256 hashed at parse time, plain text never stored
V5: import idempotent → upsert by external_id, no double-count on re-upload
V6: LLM unavailable → dashboard still renders (AI features degrade gracefully)
V7: ∀ metric computation → SQL/Go, never LLM
Evidence behind the claims above, not just assertions.
Narrative groundedness (V3). Every number a narrative states is checked against the exact metrics passed to the LLM (groundedness.go), with one repair retry and a deterministic, non-LLM fallback if the model can't stay grounded. Metrics are converted to the units a narrative would actually state (dollars, percentages) before being sent, so there's never a unit mismatch for the model to "hallucinate" across. Verified against a live local model (Gemma, 26B) on three fixtures — a normal store, a zero-order edge case, and an outlier-heavy dataset — all three fully grounded, zero invented numbers.
V6 — LLM down, dashboard still renders. Verified by killing a real listener standing in for Ollama/LM Studio and calling all three AI entry points against it: narrative generation, text-to-SQL, and content generation each degrade to a usable, non-error result rather than failing. The dashboard's metrics panel runs off a separate query from the AI insight panel, so a missing insight can never block it from rendering.
Cost & latency. Every AI call logs latency and token usage, tagged by call type. A real batch (1 narrative, all 3 content-studio kinds, 2 text-to-SQL generations) against a local 26B model averaged 103s and ~1,350 tokens per call. $/1,000 ops: $0.00 — self-hosted inference has no metered API cost; latency and local compute capacity are the real constraints for a model this size, not dollars.
SQL guardrail. Stress-tested past the existing 20+ malicious-input suite. Two real gaps were found and closed: a stacked-statement bypass (SELECT 1; SELECT 2 — no blocklisted keyword, so nothing rejected it before) and a cross-store leak on variants (no store_id column of its own, so a bare SELECT ... FROM variants returned every store's rows — now filtered via a join through products). A CSV-import prompt-injection scenario (malicious product title/description) was also tested and confirmed inert: ingested product data has no path into the SQL prompt, and reaches content-studio only as free text that's never executed.
To-Do:
- Verified against a real store's live data — not done. Only sample CSVs have been used; this needs an actual store export and can't be simulated here.
- V6 is confirmed at the API layer; not yet verified with a running browser/Playwright session.
- No cost/latency baseline for Ollama — only LM Studio's 26B model was benchmarked; a smaller non-reasoning model would likely differ meaningfully.
- The guardrail is text-based, not AST-based — it trades a rare false positive (a semicolon inside a string literal gets blocked) for no false negatives on statement smuggling.
# Go — ingest parsers + analytics + AI guardrails
cd packages/ingest-core && go test ./... -v -race
cd ../../services/api && go test ./... -v -race
# Web typecheck
pnpm turbo run typecheck
# E2E (requires running stack)
cd apps/web && pnpm exec playwright testcartography_code/
├── apps/
│ ├── web/ # Next.js 14 dashboard + chat + content studio
│ └── mobile/ # Expo SDK 56 mobile app
├── services/
│ ├── api/ # Go GraphQL API (gqlgen)
│ └── worker/ # Go ingestion + embedding jobs
├── packages/
│ └── ingest-core/ # Shopify / Amazon / WooCommerce CSV parsers
├── deploy/ # Docker Compose + Dockerfiles + Postgres init
└── sample-data/ # Shopify CSV exports for local testing