Architecture
One typed backend, multiple clients. The v2 platform is designed around a single Convex deployment that is the source of truth for schema, business rules, and realtime state, with thin reactive clients on top.
System shape
The five load-bearing seams
The rewrite is organized around explicit seams — places where an implementation can be swapped without touching feature code.
1. Tenant isolation: orgQuery / orgMutation
Every org-scoped Convex function is built with the custom wrappers in packages/backend/convex/lib/functions.ts. They inject {sessionToken, orgSlug} args, resolve {user, org, membership} into context, and throw typed errors on failure. Feature code never filters by orgId manually — it reads ctx.org. This is the single place tenancy is enforced.
2. AuthZ: role hierarchy + policy board
A minimum-role hierarchy (owner > manager > supervisor > employee > volunteer > customer) combined with a per-org role-policy board: each sensitive action (e.g. orders.refund, visits.checkin_override) has a default minimum role that orgs can override in Settings → Roles. Enforcement happens server-side in requirePolicy; the UI's role gates are UX-only. See Roles & Permissions.
3. Identity: local credentials behind the Clerk seam
Today auth is a local PBKDF2 credential provider plus a sessions table issuing 14-day opaque bearer tokens. This sits exactly where Clerk will slot in for production — the session-resolution helper is the only thing that changes. See Auth & Sessions.
4. Visit lifecycle: one transition() function
Visits move through planned → due_in → in_house → overdue → completed (plus cancelled / no_show) via a single server-enforced state machine (VISIT_TRANSITIONS in the domain package). A 15-minute cron sweeps time-based transitions (due-in windows, overdue grace periods) using each org's configurable check-in/out windows.
5. Money: integer cents + provider interface
All money is integer cents end to end. Payments go through a provider interface: dev (instant simulated settle), manual (cash/desk), and stripe (production driver with SetupIntents, off-session PaymentIntents, and idempotency keys). fluidpay is reserved in the enums but has no driver yet.
Clients
- apps/web — the product. All pages are client components using live Convex queries (
useOrgQuerywrappers). Three surfaces in one app: the staff app under/o/[slug]/*, the customer portal at/o/[slug]/portal(renders without the staff shell), and the platform-admin landlord surface at/platform. See Web App. - apps/ios — SwiftUI customer companion. A single
@Observable AppStoremirrors the Convex schema with mock data today; each intent method is annotated with the Convex mutation it maps to 1:1. See iOS App. - apps/website — standalone marketing site with zero backend dependencies. See Marketing Website.
- apps/admin — this console. Standalone, no backend dependency; serves internal documentation and the screen-flow map.
Realtime as a default
Convex queries are live subscriptions, so realtime isn't a feature that was built — it's the default. The facility board (board.live), dashboard (dashboard.daily), and message threads update across sessions with no refresh; the Playwright harness explicitly verifies a second logged-in session sees board changes without reloading.
Platform (landlord) surface
Outside org scoping there is a small landlord layer: public platform.directory (marketing directory), platform.apply (tenant applications from the /apply wizard), and platform-owner-only application review that provisions a new org on approval. Platform-owner status = holding an active owner membership in any org.