7.1 KiB
Client Area — brief
Phase: P0
Audience: Client-facing (prospects + booked clients)
Stack: React web SPA (Vite) deployed per provider brand domain; data from platform-api :3060 on black
v2 reference: codebase/@features/vip/ + codebase/@features/clients/frontend-public/ in lilith-platform.live
The Client Area is the provider's outward-facing portal for prospects and clients. It holds the documents the provider sends to clients (quotes, invoices, future doctypes), the login surface they authenticate through, and the confirmation surface where they select options and receive deposit instructions. Clients never see the provider portal, the AI copilot, or any operational tooling — those are strictly internal.
What it is
One React SPA, served at each provider's brand domain. Three tiers of surface:
- Auth surface (
/login) — phone-number entry, OTP request and verify, magic-link auto-submit from SMS, session cookie issuance. - Document surfaces (
/documents/:year/:slug) — read-only presentation of provider-authored content: pricing tiers, itinerary, terms, payment instructions. Clients can select options but cannot edit document content. - Confirmation surface (
/documents/:year/:slug/confirmed) — after a client selects an option, they see deposit instructions grouped by payment-method sections (Crypto / Cash & prepaid / Transfers). No further interaction after this point; the provider drives next steps.
What it is not
- Not admin. The provider's own management interface (content authoring, document creation, client CRM, analytics) lives in
provider-portal. Client Area is read-only from the client's perspective. - Not AI copilot.
ai-copilotis a provider-facing iOS + web surface. Clients have no AI interaction. - Not messaging. Inbound messages from clients (iMessage, SMS, email) flow through
mac-sync→messenger→engagement-ingestor. Client Area surfaces only static documents; it has no real-time chat. - Not a marketplace. No browsing, no discovery, no public-facing listings. Clients reach the Client Area through a direct link from the provider (SMS, email, iMessage).
Who uses it
| Actor | Role |
|---|---|
| Prospect / Client | Receives a link from the provider; authenticates via SMS OTP; reads and responds to documents |
| Provider | Authors documents via provider-portal; sends the magic-link URL via messaging; monitors selections via provider-portal inbox |
platform-api |
Validates OTP, issues session cookies, serves document payloads, records responses |
mac-sync |
Dispatches OTP SMS from the provider's Mac via the macOS cellular path |
Multi-tenancy
Person-only providers (standalone)
The common case. The provider has no org; every client record is scoped to provider_id (the Person's UUID from users). The Client Area brand domain maps directly to that provider_id via a provider_brand_domains table.
Phone uniqueness is per (provider_id, phone_e164). The same phone number can be a client of two different providers without collision.
OTP rate limits are per (provider_id, phone_e164) — three send attempts and five verify attempts per ten minutes, within one provider's scope. A single human being a client of two providers does not consume the other provider's rate budget.
Org-overlay providers (Org context active)
When the provider's SSO session has an org_id active (DESIGN.md §5 — SSO JWT extension), documents created in that context carry org_id on the documents row. Client Area resolves the document using the same brand-domain → provider lookup; org_id is carried through to document_responses for attribution. Clients are unaware of whether they're talking to a Person or an Org entity — the brand domain is the only identity they see.
Multi-member Orgs do not change the Client Area UX. Org membership affects which provider-portal users can see a document; from the client side, the document belongs to the brand.
Isolation guarantee
platform-api enforces tenant isolation at the query level: every clients, documents, and document_responses lookup includes a provider_id filter. A valid session cookie from www.cocotte.club cannot read documents belonging to a different brand domain even if the URL path is guessed. See client-area-architecture.md §Tenant isolation for the query pattern.
Brand-domain configuration
Each provider instance of Client Area is a deploy of the same SPA, with a deploy-time VITE_PROVIDER_ID env variable and a Caddy/nginx vhost pointing to that deploy.
Provider brand domain VITE_PROVIDER_ID platform-api lookup
─────────────────────────────────────────────────────────────────
www.cocotte.club resolved at runtime provider_brand_domains.domain = req.host
clients.future.com resolved at runtime provider_brand_domains.domain = req.host
www.future.com/clients resolved at runtime same, path-prefix mode (cookie scoped to /clients)
Domain resolution happens on the API side, not the SPA. The SPA passes Host to platform-api on every request; platform-api resolves provider_id from the provider_brand_domains table. The SPA itself is stateless with respect to which provider it serves.
Cookie scope note: subdomain-mode deploys (www.cocotte.club) scope the session cookie to the bare domain (.cocotte.club). Path-prefix-mode deploys (www.future.com/clients) must scope the cookie to the path prefix (/clients). These are deploy-time config differences; the auth code is the same.
API routing in path-prefix mode: when the SPA is mounted at /clients, its API calls must be prefixed to match: platform-api must be reverse-proxied at /clients/api/client-area/... (Caddy handle /clients/api/* → strip prefix → upstream). Subdomain-mode deploys proxy /api/client-area/* directly. The ClientAreaModule routes are prefix-agnostic; only the vhost config changes.
Open question: should platform-api reject requests from Host values not present in provider_brand_domains, or should there be a fallback to a dev-mode bypass (e.g. any *.apricot.local host)? Dev bypass is convenient but adds a code path that must be gated on NODE_ENV !== 'production'.
Related
client-area-architecture.md— data model, endpoints, session semantics, ASCII OTP sequence diagramclient-onboarding.flow.md— end-to-end onboarding path from invite SMS to first documentclient-document-types.contract.md— doctype contract,doc_nosequencing, extension rulesclient-quote-confirm.flow.md— option-select + payment-rail-picker UX- DESIGN.md §2 — Person-first, Org-as-overlay tenancy model
- DESIGN.md §5 —
orgs,org_members, SSO JWT extension - INFRA.md §1 — host placement (
platform-apion black, SPA on vps-0,mac-syncon plum)