← Back

Project deep dive

PathSix CRM

A production multi-tenant SaaS CRM built from scratch for real clients in the field services industry. Not a demo. Not a tutorial clone. Designed, architected, and shipped — with AI as a genuine collaborator throughout.

ViteReactTypeScriptQuartSQLAlchemyPostgreSQLAlembicTailwindFly.ioVercelBackblaze B2
View live site ↗
PathSix CRM dashboard

Overview

PathSix CRM handles the full client lifecycle for small business operators — from the first lead contact through conversion, active project management, and subscription tracking. It was built because the off-the-shelf options were either overbuilt for enterprise or too generic to fit how field services businesses actually operate.

Every data model, API route, and UI flow was designed intentionally. Multi-tenant isolation, white-label configuration, and a real reporting suite weren't bolted on after the fact — they were architectural decisions made at the start.

AI was a genuine collaborator throughout, not autocomplete. Architecture reviews, edge case discovery, query design, and security considerations all happened in conversation. That changed what was achievable in the timeline without cutting corners on quality.

Core Features

What it actually does

Multi-tenant architecture

Complete data isolation enforced at the query layer on every endpoint — not just the auth layer — via per-request tenant_id scoping. No possibility of cross-tenant leakage.

White-label tenant config

Each tenant's branding, labels, lead statuses, business types, currency, date format, and feature toggles are stored as a JSON blob in PostgreSQL and surfaced to the frontend via a typed useCRMConfig() hook. Full customization with zero code changes or redeployments.

Lead → Client conversion workflow

Full lifecycle state machine: lead capture, assignment, follow-up, conversion to client, project creation, and historical analytics. Interaction history transfers automatically on conversion. Source lead tracked for funnel attribution.

13 reporting endpoints

Sales pipeline, lead source conversion rates, revenue by client, MRR/ARR forecasting, follow-up risk, client retention, project performance, upcoming renewals, and converted-lead traceability — covering the full business intelligence surface.

Role-based access control

Many-to-many User ↔ Role model with a reusable @requires_auth(roles=[...]) decorator. Fine-grained capability roles (e.g. file_uploads) beyond simple admin/user splits. JWT with role claims embedded in the token payload.

CSV / XLSX lead import

File preview, dynamic column mapping, encoding detection with fallback, per-row validation, failure capture with diagnostics, and assignment email notifications — built for real-world messy datasets.

Calendar integration

FullCalendar with event styling by status and entity type, drag-and-drop date updates, and backend-generated .ics downloads. Direct Google Calendar and Outlook links from interaction modals.

Encrypted backup pipeline

pg_dump → GPG AES-256 encryption → SHA-256 checksum → Backblaze B2 upload with metadata tracking and retention cleanup. Restore pipeline includes checksum verification, pre-restore safety snapshot, and durable audit logs written to B2.

Technical Highlights

Under the hood

Architecture

  • Entity lifecycle state machine: created → assigned → soft-deleted → restored → purged and lead → converted → client
  • Many-to-many Role model with reusable auth decorators
  • Storage abstraction layer supporting local disk and S3-compatible vendors — decoupled from application logic
  • Alembic migrations for all schema changes: foreign keys, JSON columns, composite indexes, new tables

Backend

  • Async Python API with Quart (async Flask) and SQLAlchemy ORM — async/await throughout
  • Pydantic validation schemas on every create/update endpoint with structured error feedback
  • N+1 eliminated via bulk entity hydration on activity log aggregation
  • Composite indexes on tenant_id, deleted_at, assigned_to, created_at, contact_date, project_id
  • Global cross-entity search across clients, leads, projects, accounts, and users with field-level match metadata

Frontend

  • React + TypeScript SPA (Vite) — typed throughout with Zod schemas and react-hook-form on all flows
  • Dynamic tenant-aware UX via useCRMConfig() — labels, statuses, icons, and feature toggles adapt per tenant
  • Admin workspaces with cross-user filtering, bulk actions, and inline edit flows
  • Persistent per-user pagination, sort order, and view preferences synced to a backend preferences API

Security

  • Stateless JWT (Authlib) with 30-day expiry and role claims in the token payload
  • bcrypt password hashing + timed itsdangerous reset tokens for account recovery
  • IP-based sliding-window rate limiting on /login and /forgot-password
  • Cache-Control: no-store on all sensitive list responses

Deployment

  • Backend: Fly.io with region pinning (iad), TLS enforcement, auto-start/stop, and startup DB warm-up retries
  • Frontend: Vercel with multi-origin CORS allowlist for production, staging, and local
  • Sentry in both backend and frontend for runtime error capture and performance telemetry

Engineering Challenges

Where it got interesting

01

Tenant isolation without query bloat

Every query touches tenant_id. The challenge was enforcing this at the query layer on every endpoint without making it a discipline problem — it needed to be structural. The solution was centralizing scoping in SQLAlchemy base queries so it was impossible to forget.

02

Project ownership inheritance

Visibility rules for projects were ambiguous: project-level assigned_to takes priority, but falls back to the linked client or lead's owner. Getting this logic right without creating access gaps required careful query design and explicit test cases for each ownership path.

03

Cross-entity search at speed

A global search across 5 entity types with field-level match metadata could easily become a performance problem. The solution used targeted indexed queries per entity type with a result union, avoiding full-table scans while still returning useful match context.

04

Async correctness across the stack

Quart is async Flask, which means mixing sync and async code breaks silently. Every route, helper, and I/O operation — including file handling, email notifications, and database calls — had to be consistently async or the event loop would block.

← Back to portfolioView live site ↗