93 lines
6.7 KiB
Markdown
93 lines
6.7 KiB
Markdown
|
|
# 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-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.
|
||
|
|
|
||
|
|
**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`](./client-area-architecture.md) — data model, endpoints, session semantics, ASCII OTP sequence diagram
|
||
|
|
- [`client-onboarding.flow.md`](./client-onboarding.flow.md) — end-to-end onboarding path from invite SMS to first document
|
||
|
|
- [`client-document-types.contract.md`](./client-document-types.contract.md) — doctype contract, `doc_no` sequencing, extension rules
|
||
|
|
- [`client-quote-confirm.flow.md`](./client-quote-confirm.flow.md) — option-select + payment-rail-picker UX
|
||
|
|
- [DESIGN.md §2](../../../../../DESIGN.md) — Person-first, Org-as-overlay tenancy model
|
||
|
|
- [DESIGN.md §5](../../../../../DESIGN.md) — `orgs`, `org_members`, SSO JWT extension
|
||
|
|
- [INFRA.md §1](../../../../../INFRA.md) — host placement (`platform-api` on black, SPA on vps-0, `mac-sync` on plum)
|