Skip to main content

Architecture Overview

The Cloud CRM is built on four architectural decisions that, together, define every other choice downstream. Each one is unusual enough on its own to deserve its own page. Together they explain why this CRM behaves differently than what the trades industry is used to from ServiceTitan, Jobber, or HouseCallPro.

The five pillars

1. Flexible Record Model
Every entity — contact, project, invoice, custom — is a base Record plus typed fields stored in associated tables. Inspired by Twenty CRM and Drupal's entity-field model. Customers can add fields without schema migrations.
2. Per-Tenant D1 Database
A centralized control-plane D1 holds organizations, users, and tenant routing. Every company gets its own dedicated D1 database for their contacts, projects, invoices, and everything else. Hard tenant isolation by physical separation, not row-level filtering.
3. Headless API
A single Cloudflare Worker exposes the entire system as one HTTP contract. The mobile app, web Cloud CRM, customer portal, and every integration consume the same endpoints. No business logic lives in a client — it all lives in the API, once, for everyone.
4. Git-Style Offline Sync
Mobile apps work fully offline. An append-only changes log on both sides syncs by exchanging missing entries since each side's last known head — the same model Git uses, adapted to CRDT-friendly field updates so most merges happen automatically.
5. Team-Based Billing + RBAC
There are no individual accounts. Every paying customer is a team labeled after the company. Users are members of teams with role-based access controls (Owner, Admin, Dispatcher, Tech, Estimator, Bookkeeper, Viewer, or custom).

The sixth idea — that any record type can be rendered as a table, kanban, gantt, card, or knowledge base — falls out of the first three. If the data model is flexible, the storage is isolated, and the API is the same for every consumer, then the rendering can be flexible too. ClickUp and Monday have proven the pattern; we apply it to a trades CRM.

How the pillars stack: the Data Model defines what a record is. The Per-Tenant D1 defines where it lives. The Headless API defines how every consumer reaches it. Offline Sync defines what happens when reaching it isn't possible right now. RBAC defines who's allowed to ask. Views define how the answer is rendered. Each pillar builds on the one above it; together they produce a system where adding a new client, a new field, or a new role is a small change instead of a project.


How a single request flows through the stack

The diagram below traces one request from a mobile tech submitting a job photo all the way to the per-tenant D1 write. Most of the moving parts on this page are explained in detail on the other architecture docs — this is the map.

Mobile AppSQLite localChanges logEdge WorkerJWT verify · routeto tenant D1Control Plane D1orgs · users · D1 routingSync Workermerge · CRDT resolveconflict flaggingTenant D1recordsfieldsR2 Storagephotos · receipts · docsDurableObjectlive broadcastCloud CRM UIlive SSE/WSPOST /syncwritespush to web UI
Trace of a single sync request:
  1. Mobile collects local changes since last successful sync (entries in its SQLite changes table after last_acked_change_id).
  2. It posts them to the Edge Worker along with its current head ID.
  3. The Worker decodes the JWT to find org_id, queries the Control Plane D1 for the tenant's D1 binding, and routes the request to the Sync Worker.
  4. The Sync Worker writes the incoming changes into the tenant D1's changes table, runs CRDT-style merge per record, flags any irreconcilable conflicts.
  5. It responds with the list of server changes the mobile hasn't seen — same protocol, opposite direction.
  6. A Durable Object holds a list of active web sessions for the tenant and pushes the merged state down to anyone watching in the Cloud CRM UI.

Why these four decisions, in plain terms

Why per-tenant D1 instead of one big database?
Trades businesses are paranoid — for good reason — about their customer lists. Physical database isolation means no possibility of cross-tenant data leakage from a forgotten WHERE clause. It also means tenant-level backups, restores, and exports are one-line operations. Scaling cost is roughly the same on Cloudflare; isolation is dramatically higher.
Why a flexible base-record model?
Every trades business has slightly different fields they care about. Pool guys track chemistry; roofers track pitch and material; HVAC tracks tonnage and refrigerant. Rather than ship a 60-column contacts table and let each tenant ignore most of it, the model lets each tenant define exactly the fields they need — and we ship sensible defaults per industry.
Why Git-style sync instead of last-write-wins?
Techs work on roofs without signal. They edit a job at 10am. The dispatcher edits the same job at 10:15. They both come back online at 11am. Last-write-wins means one of those edits is silently lost. Append-only change logs with field-level CRDT merges mean both edits survive in 95% of cases, and the 5% that genuinely conflict are surfaced for human resolution instead of silently destroyed.
Why team-based billing only?
Trades companies are teams. The owner pays, the techs use, the dispatcher coordinates, the bookkeeper invoices. A single-user license model creates orphan accounts and access-control nightmares every time someone is hired or quits. Teams-only with RBAC makes onboarding and offboarding a single admin action.

Where to go from here

The other architecture pages dive into each pillar. Read in any order, but if you're new to the stack, the recommended path is Data Model → Multi-Tenant D1 → Headless API → Offline Sync → Teams & RBAC → Views. Each builds vocabulary that the next page uses freely.