Architecture
Request path in deployment
wrangler.jsonc: main: worker/index.ts, assets from ./dist with not_found_handling: single-page-application, and run_worker_first: ["/rest/*"] so only API calls hit the Worker. The Worker strips any client-sent X-Cf-Access-Jwt, copies Cloudflare's Cf-Access-Jwt-Assertion into it, and returns 404 for anything that is not /rest/*.
API client (src/api/)
client.ts:apiFetchandapi.get/post/patch/deletehelpers. Enforces trailing slashes, attaches the Bearer token, refreshes once on 401 then retries, throwsApiErrorcarryingX-Request-ID, and shows an error toast for everything except 400 and 409 (whose bodies drive the UI).errors.ts,query.ts(query-string helpers),download.ts(CSV on click),directUpload.ts(putToPresignedUrlwith progress for R2).- Documented in
src/api/README.md.
Authentication and capabilities (src/features/auth/)
authStore.ts: access token in memory; refresh via the HttpOnly cookie;POST /rest/api/token/,/refresh/,/logout/,/rest/auth/google/;GET /rest/crm/me/returns the caller's capabilities and scope.access.tsdefines the capability names (contacts.view,contacts.edit,contacts.bulk,notes,activity,map,groups.manage,geography.manage,campaigns,templates,events,store,resources,access.manage) and scopes (all,region,areas,self).routeGate.tsxwraps each feature's routes;AppShellhides navigation the user cannot use. The backend applies the same rules, so the UI gating is a convenience, not the control.AdminAccessCardon a contact (capabilityaccess.manage) grants capabilities and scope to the linked app user.
Concepts and the full contract: docs/CRM_FRONTEND_GUIDE.md §1–§3.
Server state
TanStack Query 5 for every read; mutations invalidate the relevant query keys. Long-running backend jobs (GeographyAssignRun, AI template drafts, campaign sending) are polled with refetchInterval until they finish. The devtools are available in development.
Feature modules
| Feature | Notable pieces |
|---|---|
contacts | ContactFilterBar, ContactsListPage, ContactDetailPage tabs in tabs/, BulkAssignDialog, LinkUserDialog, AdminAccessCard |
map | PointsLayer, heat.ts, SalesLayer (geo-sales), GeographyOverlay, DrawTools (Geoman), FilterPanel, LayerControl, ShapePanel, MapAreaDialog; base layers in layers.ts |
geography, shapes | Region/Area polygons and saved shapes |
templates | DesignEditor (blocks), RichTextEditor (Tiptap), CodeMirror HTML, AiTemplateDialog, ImageLibraryDialog, VariablesMenu, SendTestDialog |
campaigns, audience | Wizard steps, AudienceBuilder, SendConfirmDialog, SendingProgress, ReportChart, RecipientsTable, devGuard.ts |
events | Recurrence, ticket tiers, registrations, check-in, Google connect, Meet attendance |
store | Orders, Deposco-only orders, inventory, Charts.tsx (Recharts) |
resources | ResourceForm, MediaCard, CoverCard, probeDuration.ts; upload flow in Cloudflare R2 |
Dev send guard
features/campaigns/devGuard.ts: when VITE_API_BASE_URL is the dev backend, the send button is disabled and a banner shown unless every audience group is in VITE_DEV_SEND_GROUP_IDS. Remove only when the backend allow-list alone is judged sufficient.