There is exactly one backend in this system. A single Cloudflare Worker codebase exposes every capability of the CRM as an HTTP API. The mobile app uses it. The web Cloud CRM UI uses it. The customer portal uses it. Future integrations — Zapier, your accountant's read-only dashboard, an AI tool you haven't built yet — will use it. There is no "mobile backend" and "web backend" and "portal backend." There is one backend, and three (eventually many) clients.
This decision sounds boring until you've watched what happens when a team breaks it. Two backends drift. The mobile codebase gets a special-case sync rule that the web doesn't have. The portal team rewrites permission checks because they couldn't find the originals. A year later, nobody can answer "what does this system do when a tech edits an invoice while offline?" because the answer depends on which client they're using. Headless avoids the whole category.
Business logic — sync, conflict resolution, RBAC, billing, automations — lives in the Worker and only in the Worker. Clients render what the API tells them; they don't compute it independently.
Identical contracts
Every client sees the same endpoints, the same JSON shapes, the same error codes. A behavior change ships once and applies everywhere simultaneously.
New clients are cheap
Adding a desktop app, a Slack integration, an Excel plugin, or whatever comes next is one well-documented spec away. The backend doesn't need to change to support a new consumer.
The diagram below names every current and planned consumer of the API. Each is just an HTTP client. The lower half of the diagram is the same on every consumer; the upper half is whatever shape the client wants to be.
A handful of resource families cover everything the CRM does. Each one is exposed identically to every client; the difference between clients is which routes they're allowed to call, not whether the routes exist.
The API doesn't change for who's calling. What changes is which routes a given JWT can hit, which fields are returned, and how aggressively the client caches.
Mobile App
Heavy use of /sync: the app spends most of its life in the change-log exchange. CRUD reads/writes go to the local SQLite first; /sync reconciles in the background.
Files via R2 directly: /files issues a signed PUT URL, the device uploads the photo straight to R2, then notifies the API with the resulting key.
RBAC: tech-scoped JWT. Most endpoints return 403 unless the request is for the user's own jobs.
Cloud CRM Web UI
Heavy use of /records and /views: dashboards are list-and-filter heavy. The UI requests rendered views via the view ID; the API returns the materialized result.
Real-time via /events: opens a Server-Sent Events connection on load. Receives "you have new changes" hints, then pulls deltas via /sync.
RBAC: per-role JWT. Owner/Admin see everything; Dispatcher sees ops; Bookkeeper sees money.
Customer Portal
Restricted to /portal/*: portal-issued JWTs only validate against the portal namespace. The customer sees their own jobs, their own invoices, their own scheduled visits.
Read-heavy, write-narrow: reads job state, schedule, invoices. Writes are limited to "book appointment," "pay invoice," "message tech," "request reschedule."
RBAC: customer-scoped JWT. The sub claim is the contact_id, not a user_id. Field-level visibility is the strictest of any consumer.
Integrations & AI
Long-lived API keys: integrations use API-key-issued tokens with explicit scopes (Stripe sync, QuickBooks export, etc.) instead of user-issued JWTs.
Webhooks both ways: can register inbound webhooks (Stripe → API) and consume outbound ones (API → Zapier).
AI Receptionist: a Worker peer, calling the API just like an external system would. No special-case code paths.
Why headless makes the rest of the architecture work
The four pillars on the overview page are easier to keep honest because there is exactly one place where they're implemented.
Flexible Record ModelA new field_definition added by an admin appears in every client immediately — no client release required. The API materializes records the same way for everyone, so mobile, web, and portal can't drift on what "a Contact looks like."
Per-Tenant D1All clients talk to one API; the API does the JWT-to-tenant routing once. A new consumer doesn't need to learn the binding model — they just authenticate and the data scopes itself.
Git-Style SyncThe change log is the protocol. If the protocol is wrong, every client is equally wrong; if the protocol is right, every client is equally right. There is no second sync implementation maintained in a different repo by a different team.
Team-Based RBACPermissions are enforced at the API layer. The Cloud CRM UI doesn't have to re-implement role checks; the mobile app can't accidentally allow an action the portal disallows. Roles change once and apply uniformly.
Multi-View RenderingViews are queried by ID; the API returns the materialized view (filtered, sorted, grouped, projected to visible_fields). Clients render the result — they don't compute it. A new view type added server-side becomes available to every consumer at once.
A handful of conventions hold across every endpoint. Documenting them once here means every individual route doc can omit them.
JSON only
Every request and response is JSON. No XML, no form-encoded, no multipart except for /files upload chunks. Predictable parsing on every client.
UUIDv7 IDs everywhere
Every entity ID is a UUIDv7 — globally unique, lexicographically sortable, generatable client-side without server coordination. Lets mobile create records offline with stable IDs.
Unix epoch ms timestamps
Every datetime is an integer of milliseconds since the Unix epoch. No ISO strings, no timezone wrangling at the wire — clients localize on display only.
Cursor pagination
List endpoints page by opaque cursor, not by offset. Stable under inserts, cheap on the database, identical interface across resource families.
Versioning by Accept header
v1 is implied. Future versions arrive via Accept: application/vnd.crm.v2+json. No /v2/ URL prefixes — the URL space stays stable while the contract evolves.
Errors are typed
Every error response carries a stable code (e.g. CONFLICT_REQUIRES_REVIEW), a human message, and a context object. Clients branch on code, never on the message text.
Idempotency keys on writes
Any POST/PATCH/DELETE accepts an Idempotency-Key header. The Worker dedupes retries against a short-lived KV cache. Network retries are safe by default.
Field selection by default
Every list endpoint accepts ?fields=a,b,c to project only the requested fields. Mobile bandwidth respect costs nothing on the server and saves a lot on the wire.
The risk of one backend serving many clients is that a breaking change in the API breaks every client at once. The risk is real; the mitigation is structural.
1Additive by defaultNew fields, new optional parameters, new endpoints — always safe. The vast majority of changes fall here and ship in days.
2Deprecate before removingTo remove or rename anything, mark it deprecated in the OpenAPI spec, return a Sunset header, and notify clients via the integrations dashboard. Minimum 90 days before removal.
3Versioned breaking changesGenuinely incompatible changes land under a new Accept header version. Old clients keep talking v1 while new clients adopt v2. The Worker dispatches per-version handlers in the same codebase.
4Contract tests on every PREach client repo (mobile, web, portal) runs contract tests against the API spec in CI. A PR that breaks any client's contract fails CI on the backend — visibility before merge, not after deploy.
5Synthetic monitors on every endpointCloudflare Workers Health Checks call every public endpoint every minute from multiple regions. A regression in shape or status code pages the on-call within seconds, not after a customer complains.
Each piece of the architecture is interesting on its own. The reason they compose is that they share one front door.
A new view type added to the Worker becomes available on mobile, web, and portal in one deploy.
A new RBAC permission becomes enforceable everywhere because there's only one enforcer.
A protocol fix to /sync makes every client more correct without touching their code.
A new consumer — the Excel plugin, the Slack bot, the AI agent — gets built against a stable, documented contract, not by reverse-engineering a particular client.
The CRM gets boring at the edges. The interesting things happen in one place, behind a single well-defined contract, and they happen for everyone at once. That's the headless promise.
Continue to Offline Sync for the specific protocol the /sync endpoint implements, or jump to Teams & RBAC for how the API enforces who can call what.