lilith-platform.live/codebase/@features/vip/docs
..
billing.md
dates.md
gallery.md
messages.md
README.md
settings.md
story.md

quinn.vip — Feature Overview

Domain: quinn.vip (prod), vip.quinn.apricot.local (dev) Feature source: codebase/@features/vip/frontend-client/ (Vite + React SPA, PWA) Backend: codebase/@features/api/ (shared NestJS API, port 3031)


What it is

A private, per-client portal. Each invited client gets a unique token URL (/portal/:token). Behind that URL is an encrypted messaging channel + relationship data — upcoming dates, billing history, shared photos, a personal story, and account settings.

The app is a PWA installable to the home screen. It ships multiple disguise manifests so the icon can appear as Calculator, Notes, Weather, or Fitness on a client's device.


Auth flow

/:token
  └─ getAuthStatus(token)
       ├─ hasPassword: false  →  SetPasswordScreen (first visit)
       └─ hasPassword: true   →  LoginScreen
            ├─ hasWebAuthn: true  →  auto-trigger WebAuthn
            └─ password form  →  verifyPassword(token, pw)
                  └─ onUnlocked → sessionStorage.setItem('vip_auth_ok', '1')

Session flag vip_auth_ok in sessionStorage skips the auth check on tab re-focus within the same session. Closing the browser tab clears it.


Storage keys

Key Storage Purpose
vip_token localStorage Persists the token across sessions
vip_auth_ok sessionStorage Auth bypass within a single browser session
vip_content_key sessionStorage AES-GCM content decryption key, derived at login
vip_welcomed_<token> sessionStorage Prevents welcome splash from re-showing

Encryption

Messages and sensitive content are encrypted server-side per-client. The content key is derived from the client's password via PBKDF2 (or Argon2 depending on settings). On login, verifyPassword returns the raw contentKey which is stashed in sessionStorage for the tab's lifetime.

The Settings tab exposes the encryption algorithm, KDF, iteration count, and key creation date.


Disguise mode (PWA)

public/ contains multiple manifest-*.json files. At install time, the user selects a disguise:

Disguise Manifest Icon
Default manifest-default.json Crown/star
Calculator manifest-calculator.json Calculator
Notes manifest-notes.json Notes
Weather manifest-weather.json Weather
Fitness manifest-fitness.json Fitness

DisguisePicker.tsx handles selection. The service worker (sw.js) caches the correct manifest.


API base

Env Value How
Dev (vip.quinn.apricot.local) '' (empty) Relative URLs → Vite proxy → localhost:3030
Prod (vip.transquinnftw.com) https://my.transquinnftw.com/api/v2 Set at build time in deploy.sh

The prod path works because my.transquinnftw.com/api/v2/ nginx block proxies to :3030/.

Important: every API sub-path (/invites, /relationship, /messages, /push, /referrals, /billing, /auth, /settings) must be listed in the Vite proxy apiPaths array in vite.config.ts. A missing entry causes the Vite dev server to serve the SPA HTML instead of forwarding to the API. The dev server must be restarted after any change to vite.config.ts.


Header

The portal header renders "Quinn's {Tier} VIP Area" where tier is the reputation tier Quinn assigned (Bronze / Silver / Gold / Diamond). Falls back to "Quinn's VIP Area" when no tier is set. Tier is fetched from getRelationship() on unlock.


Tabs

Tab Internal id Doc
Messages messages messages.md
Dates reservations dates.md
Billing billing billing.md
Story story story.md
Gallery gallery gallery.md
Settings settings settings.md

Key files

Path Purpose
frontend-client/src/pages/VipPortalPage.tsx Main component — all tabs, auth states, nav
frontend-client/src/api.ts All API calls + TypeScript types
frontend-client/src/push.ts Web Push subscription registration
frontend-client/src/hooks/useMessageStream.ts SSE message streaming hook
frontend-client/src/components/DisguisePicker.tsx PWA disguise selection UI
frontend-client/src/disguises.ts Disguise config constants
frontend-client/vite.config.ts Build config, PWA plugin