A paint decision engine that helps customers choose a wall color confidently in under 60 seconds. Built for in-store dealer demos — upload a room photo, preview shades live on the walls, compare side by side, capture a lead, and sync everything to a real backend.
Live demo: paintcrm.brohammad.tech · Login: /login
| Architecture | ARCHITECTURE.md — full system reference |
| Operations | OPERATIONS.md — deploy, monitoring, runbooks |
| Roadmap | master-plan.txt — phase plan + metrics |
| Backlog | BACKLOG.md — P2/P3 only (P0/P1 closed) |
| Contributing | CONTRIBUTING.md · SECURITY.md · MIT LICENSE |
| Readiness | docs/PRODUCTION_READINESS.md · docs/TENANCY.md · docs/SECURITY_AUDIT.md |
| E2E | e2e/README.md — Playwright (runs in CI) |
| Changelog | CHANGELOG.md |
flowchart TB
subgraph Browser["Browser (offline-first)"]
APP["Vite bundle · script.js + views"]
MOD["src/ modules"]
APP --> MOD
MOD --> COLOR["color · segmentation · tint"]
MOD --> CRM["api · pagination · cost · format"]
end
subgraph Server["Express API :3001"]
MW["Helmet · CORS · rate limit · JWT auth"]
RT["13 route modules"]
LB["domain libs"]
MW --> RT --> LB
end
subgraph Store["Persistence & ops"]
PG[(PostgreSQL<br/>multi-tenant)]
RD[(Redis<br/>rate limit fallback)]
PR["Prometheus · Pino logs"]
end
APP -->|"REST · access + refresh tokens"| MW
LB --> PG
MW -.-> RD
Server --> PR
Auth lifecycle (summary): register/login → short-lived access token (~15m) + rotating refresh token (SHA-256 hash stored server-side) → transparent refresh on 401 → reuse detection revokes the session chain on token replay. Password reset uses single-use hashed tokens + optional SMTP (dev logs the reset URL when SMTP is unset).
Data integrity highlights: server-computed quote/order totals · append-only credit ledger with row-level locking · auditable inventory movements · per-tenant document numbering.
| Layer | Technologies |
|---|---|
| Frontend | Vanilla JS, Vite, Vitest, Canvas 2D, optional DeepLab wall assist |
| Backend | Node.js, Express, node-pg-migrate |
| Database | PostgreSQL with tenant-scoped queries |
| Auth | JWT access + rotating refresh, bcrypt (12 rounds), password reset, session revocation |
| Observability | Prometheus metrics, Pino structured logs, health/ready/live probes |
| Infra | Docker multi-stage, GitHub Actions CI/CD, Fly.io + Render deploy |
| Suite | Runner | CI |
|---|---|---|
| Backend API / libs | Jest + Supertest | ✅ (Postgres service) |
| Frontend units | Vitest + jsdom | ✅ |
| Browser E2E | Playwright (e2e/) |
✅ (fails build on regression) |
| Container scan | Trivy | ✅ |
Key frontend modules under test: wall segmentation, HSL tint, cost estimator, API refresh, pagination, palette ranking. Views: src/views/{customers,quotes,inventory,ledger}.js.
Who it's for: Independent paint shops running in-store colour consultations — especially where Wi‑Fi is unreliable and leads get lost on paper.
What you get today (free pilot):
| Capability | Value |
|---|---|
| 60-second wall preview | Customer decides faster at the counter |
| Lead capture + sync | No lost walk-ins when you're busy |
| Quotes → orders | Server-computed totals — tamper-proof |
| Credit ledger + WhatsApp reminder | Follow up on receivables from the same app |
| Offline-first | Works when the network doesn't |
How to start: Create an account → follow the Getting started checklist (photo → lead → quote) → invite us for feedback.
What we're validating: Do dealers use it weekly for leads and quotes? That's the gate before staff seats, automated SMS, and deeper CRM.
Use this flow for interviews, Loom recordings, or a live dealer walkthrough.
- Open paintcrm.brohammad.tech (or local
http://localhost:3001) - Sign in via Settings → Server Sync (or /login)
- Upload a room photo → smart wall mask + shade suggestions appear
- Pick a shade → live recolor with before/after + compare slider
- Contact Dealer → capture lead (syncs to backend when signed in)
- Quotes → build a quote from the shade catalog with server-computed totals
- Ledger → show customer balance + log a payment reminder
Loom tip: Record at 1080p, ~2–3 min. Narrate why (dealer demo → lead → quote → receivables), not just what you click. End on the live URL so viewers can try it.
| Step | Action | What to say |
|---|---|---|
| 0 | Show hero + dealer branding | "Offline-first — works in a shop with bad Wi‑Fi; syncs when online." |
| 1 | Upload photo | "Under 60 seconds from photo to painted preview — heuristic mask + optional ML assist." |
| 2 | Tap wall / brush / multi-zone tabs | "Dealers can fix edge cases without leaving the demo." |
| 3 | Shade search + cost estimate | "Real Asian Paints / Dulux catalog — litres and ₹ estimate for a standard room." |
| 4 | Contact Dealer → Leads inbox | "Lead captured locally and on the server with snapshot + shade breakdown." |
| 5 | Customers → timeline | "CRM lite — customer auto-linked by phone, sites/projects, session history." |
| 6 | Quote → Convert to order | "Totals computed server-side — client can't tamper with line items." |
| 7 | Ledger → reminder | "Append-only credit ledger; order totals post automatically as debits." |
| Path | Description |
|---|---|
paint-preview-app/ |
Main app — Vite-bundled HTML/CSS/JS; src/ holds tested ES modules (color, segmentation, tint, api, …) |
server/ |
Backend — Node.js + Express + PostgreSQL; 13 route modules, domain libs, 10 migrations |
paint-preview-app/react-canvas-component/ |
Reusable React/Next.js component extracting the same canvas logic |
test-scripts/ |
Playwright E2E tests and backend smoke test scaffolding |
ARCHITECTURE.md |
Authoritative technical reference (algorithms, schema, data flows) |
master-plan.txt |
Phase roadmap from Decision Engine through CRM platform |
Docker (easiest):
cp server/.env.example server/.env
docker-compose up -d
# → http://localhost:3001Local Node + PostgreSQL:
# Create a database, then set DATABASE_URL in server/.env (see server/.env.example)
cd server
npm install
npm run migrate:up
npm run start:with-migrate # or: npm start (after migrations)
# → http://localhost:3001The Express server serves the frontend and the API on a single port. Open http://localhost:3001 and use Settings → Server Sync to create an account.
cd paint-preview-app
npm install
npm run dev # Vite dev server → http://localhost:5173
# or: npm run build && npx vite previewOpen the dev URL. Everything works offline via localStorage — no account needed. For a zero-install static serve of the built bundle: npm run build then serve dist/.
- Room photo upload — works from file or camera roll on mobile
- Shade suggestions — top 5 shades derived from the dominant room color
- Live wall recolor — natural HSL-based tint that preserves texture and lighting
- Smart wall masking — software region-growing with upper-wall bias, no manual setup needed
- ML wall assist (beta) — optional DeepLab segmentation fused with the heuristic mask
- Tap to select wall — click any point on the image to lock the wall region
- Multi-wall tabs — up to 5 independent wall zones, each with its own shade
- Brush mask mode — paint or erase the mask with a live cursor showing brush radius
- Undo / redo — full history stack (up to 20 steps) per wall tab
- Edge feathering — adjustable feather radius for clean mask boundaries
- Before / after toggle — instant flip to compare original and recolored
- Compare drag slider — drag a split-view handle to reveal any proportion of two shades side by side
- Share / Export — native share sheet on supported devices, PNG download fallback
- Real shade catalog — 63 curated shades across Asian Paints, Dulux, Berger, and Nerolac loaded from
shades.json - Shade search — type any shade name, brand, or color family to instantly filter the catalog
- Contact Dealer — capture customer name, phone, email, notes, per-wall shade breakdown, and a live preview snapshot
- Local Leads Inbox — leads persisted in the browser; list, detail view, delete
- Lead package export — downloads snapshot PNG + structured
.jsonsidecar (brand + shade per wall, dealer info) - Cost estimator — estimates litres and INR cost for a standard room (40 sq m, 2 coats) on shade selection
- Session draft save / restore — workspace auto-saved; "Restore draft" lets users resume without re-uploading
- Storage safety — graceful fallback when localStorage is full
- Session analytics engine — tracks
session_start,shade_selected(with time-to-first-pick),share_exported,contact_opened,contact_savedin localStorage (up to 600 events) - Pilot Analytics dashboard — second tab inside the Leads modal; KPI cards (sessions/30d, avg decision time, contact rate, share rate) and a 7-day bar chart
- Dealer Settings — Settings button opens a modal to set shop name, dealer name, and phone; shown as branded tagline in the hero
- Dealer branding in exports — dealer info embedded in every exported lead
.json - Analytics export — Settings → Download Analytics JSON for pilot review
- Clear analytics — reset event log before handing device to a new dealer
A full Node.js + Express + PostgreSQL backend in server/ with:
| Endpoint | What it does |
|---|---|
POST /api/auth/register |
Create a dealer account — returns a short-lived access token + a refresh token |
POST /api/auth/login |
Authenticate — returns an access token (~15m) + a rotating refresh token |
POST /api/auth/refresh |
Exchange a refresh token for a fresh access token (rotates the refresh token) |
POST /api/auth/logout |
Revoke the current session's refresh token |
POST /api/auth/logout-all |
Revoke every active session for the tenant |
POST /api/auth/forgot-password |
Request reset email (always returns a generic success message) |
POST /api/auth/reset-password |
Consume reset token + set new password (revokes all sessions) |
GET /api/auth/me |
Validate token, return tenant profile |
GET /api/leads |
List all leads for the signed-in dealer |
POST /api/leads |
Create / upsert a lead (id, name, phone, shades, snapshot) |
GET /api/leads/:id |
Single lead with full snapshot |
DELETE /api/leads/:id |
Delete a lead |
GET /api/shades |
Full catalog — searchable via ?q= |
GET /api/shades/:id |
Single shade |
GET /api/dealer |
Get dealer profile |
PUT /api/dealer |
Update shop name, dealer name, phone |
POST /api/events |
Ingest a funnel analytics event |
GET /api/events/summary |
30-day funnel metrics (sessions, contact rate, share rate, avg decision time, 7-day daily breakdown) |
Frontend sync (graceful degradation):
- All leads sync to the server automatically when signed in; deletes propagate too
- Cross-device lead restore fetches preview snapshots via
GET /api/leads/:id - Dealer profile pulled from server on sign-in
- Pilot Analytics dashboard uses server funnel metrics when signed in
- Shade catalog loads from
/api/shadeswhen signed in (falls back toshades.jsonoffline) - Every analytics event is sent to
/api/eventsin addition to being stored locally - Dealer settings saved via
PUT /api/dealeron form submit - On startup, a stored token is validated and leads are merged from the server
- App stays fully functional offline — server sync is always best-effort
Server Sync in Settings modal:
- Sign In / Create Account tabs
- Live connection status chip (green "Connected" / grey "Not connected")
- Logout
When signed in to the backend:
| Endpoint | What it does |
|---|---|
GET/POST/PUT/DELETE /api/customers |
Customer CRUD (search via ?q=) |
GET /api/customers/:id/timeline |
Merged leads + preview sessions for a customer |
GET/POST/PUT/DELETE /api/sites |
Site/project per customer (?customerId=) |
POST /api/sessions |
Record preview session events on the timeline |
- Customers button in the app — browse, search, add, edit, and delete customers
- Sites/projects — add via a form on the customer detail view
- Lead capture auto-creates or links customers by phone; optional customer + site link
- Timeline on each customer — session starts, shade picks, lead captures
- Lead → customer — jump from a lead to its linked customer profile
- Offline cache — last-synced customers stay viewable offline; writes require sign-in
Quote → order flow for signed-in dealers.
| Endpoint | What it does |
|---|---|
GET/POST /api/quotes |
List (filter by ?customerId= / ?status=) and create quotes with line items |
GET/PUT/DELETE /api/quotes/:id |
Read, replace (header + items), and delete a quote |
PATCH /api/quotes/:id/status |
Move a quote through draft → sent → accepted / rejected |
POST /api/quotes/:id/convert |
Create an order from the quote and lock the quote as converted |
GET/POST /api/orders |
List and create orders (direct or via conversion) |
GET/DELETE /api/orders/:id |
Read and delete an order |
PATCH /api/orders/:id/status |
Move an order through pending → confirmed → fulfilled / cancelled |
- Quotes button in the app — a Quotes / Orders tabbed modal with a status filter
- Quote builder — pick a customer + site, add line items manually or from the shade catalog (auto-fills price/L and standard-room litres), set discount and tax rate, live totals
- Server-computed totals — subtotal, discount, tax, and total are always recomputed on the server (line totals are never trusted from the client)
- Per-tenant document numbers — sequential
Q-0001/O-0001, isolated per dealer - Convert to order — one click turns an accepted quote into an order (items + totals snapshotted); the quote is then read-only
- Status workflows — inline status controls on the quote/order detail view
- Requires sign-in (commercial data is server-only; the offline decision flow is unaffected)
Inventory basics + stock status:
| Endpoint | What it does |
|---|---|
GET/POST /api/inventory |
List (search ?q=, filter ?status=) and create stock items |
GET/PUT/DELETE /api/inventory/:id |
Read (with movement history), update metadata, delete |
POST /api/inventory/:id/adjust |
Apply a signed stock movement ({delta, reason}) |
GET /api/inventory/summary |
Counts by stock status + total stock value |
- Inventory button — item list with search, stock-status filter, and summary chips (items / low / out / stock value)
- Stock status derived from quantity vs. reorder level:
in_stock/low_stock/out_of_stock - Auditable movements — every quantity change (opening stock, receive, issue, correction) is recorded with a running balance; adjustments can't drive stock negative
- Catalog link — optionally link an item to a shade to auto-fill name/brand/price
- Per-tenant SKUs — optional, uniquely enforced only when provided
Credit ledger + payment reminders:
| Endpoint | What it does |
|---|---|
GET /api/ledger/summary |
Tenant-wide receivables snapshot (receivable, overdue amount, debtor counts) |
GET /api/ledger/customers |
Customers with an outstanding balance (?overdue=true, ?q= search) |
GET /api/ledger/customers/:id |
Full statement — balance, overdue state, entries + reminders |
POST /api/ledger/customers/:id/entries |
Record a debit (charge) or credit (payment) |
POST /api/ledger/customers/:id/reminders |
Log a payment reminder action (with balance snapshot) |
- Ledger button — receivables summary chips, an "everyone who owes / overdue only" filter, and a searchable debtor worklist
- Append-only account — every debit/credit stores the running balance after it was applied, so a statement renders without re-summing
- Order totals post automatically — creating (or converting to) an order writes a debit to the customer's account; deleting the order writes a compensating reversal so balances stay correct
- Overdue flags — a customer is overdue when they owe money and a dated charge is past due; overdue accounts sort to the top of the worklist
- Reminder actions — log a WhatsApp / call / SMS / email follow-up; each is timestamped with the balance at the time
- Requires sign-in (commercial data is server-only)
Cross-cutting improvements that make the platform production-ready at scale:
-
Paginated list APIs — every list endpoint (
customers,leads,quotes,orders,inventory,ledger/customers) accepts?limit=&?offset=and returns apaginationblock{ total, limit, offset, hasMore }. Defaults to 50/page, capped at 200. The debtor worklist has a "Load more" pager wired to it. -
Hardened auth lifecycle — short-lived access tokens (
~15m) backed by long-lived, rotating refresh tokens persisted server-side (only the SHA-256 hash is stored). Refresh rotation includes reuse detection (replaying a rotated token revokes the whole session chain), pluslogout(single session) andlogout-all(every session). The frontend refreshes access tokens transparently on401. Password policy: min 8 chars with at least one letter and one number. -
Modular, bundled frontend — built with Vite (minified + hashed assets, source maps). Pure logic lives in tested ES modules under
paint-preview-app/src/:Module Responsibility color.jsHex/RGB/HSL conversion, pixel colour reads segmentation.jsWall detection, mask grow/fuse/smooth, ML mask extraction tint.jsAlpha masks, feathering, HSL recolor blend, shade suggestions cost.jsPaint litres + INR estimate for a standard room api.jsToken storage, apiRequest, transparent refresh on401pagination.jsClient-side paging matching the backend contract format.js/ids.jsCRM HTML helpers, offline id generation 80 Vitest tests cover the engine modules;
script.js(~3,800 lines) remains the DOM/event wiring layer (view split planned). The server serves builtdist/in production (falls back to raw source for zero-build local dev).
See Demo in 60 seconds for the interview/Loom flow. For local setup:
- Start the server:
cd server && npm run start:with-migrate - Open http://localhost:3001
- Settings → Server Sync → Create Account
- Settings → Dealer Profile — shop name, dealer name, phone
- Upload photo → shades → Contact Dealer → Customers → Quote → Ledger
pip install -r test-scripts/requirements.txt
python3 -m playwright install chromium
# Start a server first, then:
python3 test-scripts/frontend_e2e_playwright.py --app-url http://localhost:3001cd server && npm start & # start server
# Register + full round-trip
curl -s -X POST http://localhost:3001/api/auth/register \
-H 'Content-Type: application/json' \
-d '{"shopName":"My Shop","email":"me@shop.com","password":"demo1234"}'cd paint-preview-app
npm install
npm test # run the jsdom unit tests
npm run build # produce the minified bundle in dist/Production runs on Render (free tier) with custom domain paintcrm.brohammad.tech. Fly.io config is also included.
Connect the repo via render.yaml — Docker build, Neon Postgres, auto-deploy on push.
# One-time
fly apps create paintcrm
fly postgres create --name paintcrm-db && fly postgres attach paintcrm-db
fly secrets set JWT_SECRET="$(openssl rand -hex 32)" DB_SSL=false
# Deploy
fly deploy # or push to main — CI deploys via the Fly GitHub ActionCI/CD (.github/workflows/ci.yml) runs backend lint/tests, frontend tests + build, a Docker build, and a Trivy scan, then deploys to Fly.io staging → production on main (gated on the FLY_API_TOKEN secret; the deploy steps no-op cleanly when it isn't set).
paint-preview-app/react-canvas-component/ contains a self-contained WallRecolorCanvas.tsx component and pixelUtils.ts you can copy into any React/Next.js project. See the component README for integration steps.
| Phase | Status | Description |
|---|---|---|
| 0 | Done | Product contract — scope, flow, success metrics |
| 1 | Done | Decision Engine — masking, recolor, compare, share |
| 2 | Done | Conversion layer — shade catalog, search, lead capture, inbox, cost estimator, session drafts |
| 3 | Done | Pilot validation — analytics engine, dealer branding, KPI dashboard |
| 4 | Done | Backend foundation — auth, lead/shade/dealer APIs, funnel event tracking, Docker, CI |
| 5 | Done | CRM Lite — customer CRUD, sites/projects, session timeline (server sync) |
| 6 | Done | Commercial modules — quote → order, inventory/stock, credit ledger + payment reminders (log-only; outbound send planned) |
| — | Done | Scale hardening — paginated APIs, refresh-token auth, Vite frontend modules + 80 unit tests, Docker CI/CD |
| 7 | Next | AI palette recommendations, dealer assistant, WhatsApp reminder delivery |
See master-plan.txt for the full execution plan with sprint breakdowns and success metrics.