cocottetech/@platform/codebase/@features/client-area/docs/client-area.brief.md
2026-05-18 19:47:03 -07:00

6.7 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:

  1. Auth surface (/login) — phone-number entry, OTP request and verify, magic-link auto-submit from SMS, session cookie issuance.
  2. 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.
  3. 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-copilot is a provider-facing iOS + web surface. Clients have no AI interaction.
  • Not messaging. Inbound messages from clients (iMessage, SMS, email) flow through mac-syncmessengerengagement-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.

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'.