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.
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
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
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.
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.
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.
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.
