Browser storage minimization and server authority
This page implements the browser storage minimization program: keep only intentional browser storage for UI and non-authoritative tab helpers; move business, security, and audit state to server authority (prego-control-plane D1, Auth session or JWT, Frappe ERP).
Related: Repo responsibility matrix, How to add an API, Tenant and trust boundaries.
1. Goals and principles
- Single source of truth: Tenant lifecycle, onboarding, billing, provisioning, and permissions must not rely on the browser as the final authority.
- Browser storage roles (allowed): (1) pure presentation preferences, (2) short-lived opaque tokens or correlation IDs that the server issued or validates, (3) performance or offline cache that is not the business ledger.
- Repo boundaries: Orchestration and CP D1 live in prego-control-plane; ERP domain in prego_saas; gateway in prego-zuplo; Next apps in Prego monorepo.
flowchart LR
subgraph browser [Browser_allowed]
UI[UI_prefs_theme_sidebar]
Tab[Tab_ephemeral_opaque_token]
Cache[Cache_PWA_optional]
end
subgraph server [Server_authority]
CP[(CP_D1)]
ERP[(Frappe)]
Auth[Auth_session_JWT_KV]
end
UI --> browser
Tab -->|HTTPS_API| server
CP --> Auth
ERP --> CP
2. Inventory — whitelist and blacklist by surface
2.1 Whitelist (intentional browser storage after the program)
| Category | Examples | Notes |
|---|---|---|
| Pure UI | Theme, sidebar open state, marketing banner dismissed, location prompt shown | Low risk; optional sync to profile later. |
| Consent UX | PREGO_CONSENT cookie, prego_cookie_consent_v2 local mirror | Immediate UI; optional server audit log in a separate phase. |
| Non-authoritative tab helpers | Opaque continuation token string, OTP-step email (until server confirms) | Server owns verification and row state. |
| PWA | Workbox Cache API, IndexedDB for cache expiration | Not business data. |
2.2 Blacklist (must not be sole authority for routing or security)
| Pattern | Risk | Target authority |
|---|---|---|
localStorage onboarding completion flags without server check | User can tamper | CP D1 onboarding step plus API guard. |
sessionStorage tenant id, plan tier, provision job as only source | Spoofing, desync | tenants_master, subscriptions, provision_jobs in CP (or existing tables). |
Long-lived Bearer in sessionStorage for operator tools | XSS exfiltration | Short-lived session or HttpOnly cookie pattern. |
sessionStorage employee id or email as permission | Client forgery | JWT or server session claims only. |
2.3 Per-app inventory (current keys — migration targets)
www (apps/www)
| Storage | Keys (representative) | Target |
|---|---|---|
| localStorage | prego_locale, prego_country | Cookie plus Accept-Language; optional profile in CP when logged in. |
| localStorage | prego_cookie_consent_v2 | Keep for UX; optional audit append-only log. |
| sessionStorage | prego_manual_approval_ct, prego_manual_approval_trial_email, prego_manual_approval_draft_recovery_used | Minimize: continuation maps to CP continuation_secret; email and draft live in D1 manual_approval_requests. |
admin-web (apps/admin-web)
| Storage | Keys (representative) | Target |
|---|---|---|
| localStorage | prego_tenant_id, prego_onboarding_company_done, prego_onboarding_launch_done, prego_signed_in | CP D1 user or tenant onboarding state; dashboard guard from API. |
| sessionStorage | Checkout and provision hints (SS_CHECKOUT_*, SS_PROVISION_*, company draft JSON) | CP rows: onboarding draft, funnel trace with TTL, provision correlation. |
| sessionStorage | prego_auth_pending_email, prego_onboarding_verified_email | Short-lived only; canonical email from /auth/me and CP user. |
client-web (apps/client-web)
| Storage | Keys (representative) | Target |
|---|---|---|
| Cookies | PREGO_SESSION, PREGO_JWT, PREGO_DEVICE_ID, PREGO_EID, etc. | Remain server-validated; reduce duplicate state in sessionStorage. |
| sessionStorage | prego_auth_email, prego_auth_employee_id, prego_provision_trace_id, demo flags | Prefer JWT claims and headers; trace from server job or URL once. |
| localStorage | my-ai-*, last_checkin_data, locale | Production: server chat and ERP attendance; local only for dev mock or offline queue (documented). |
prego-control-plane (admin SPA and legacy HTML)
| Storage | Keys | Target |
|---|---|---|
| sessionStorage | cp_internal_key | Replace with short-lived operator session (see §8). |
3. CP D1 — onboarding, draft, and provision correlation (draft model)
Owner: prego-control-plane.
3.1 Proposed entities (draft — align with existing tenants_master, provision_jobs, Stripe webhooks)
| Concept | Suggested shape | Purpose |
|---|---|---|
| Onboarding state | Per user_id + tenant_id (or pending tenant): current_step enum (company, review, launch, complete), company_draft_json, updated_at | Replace ad hoc localStorage and SS_ONBOARDING_COMPANY_DRAFT. |
| Checkout funnel | Row or columns: stripe_checkout_session_id, plan_tier, region, trace_id, expires_at | Replace SS_CHECKOUT_* as source of truth; client passes id or short token only. |
| Provision correlation | Existing provision_jobs + trace_id column or join | Align with prego_provision_trace_id and admin session hints. |
3.2 Entity relationship (CP D1 — planning reference)
Server authority for onboarding and billing centers on prego-control-plane D1. The following diagram is a logical view for migrations and internal APIs; table names may map to existing or new migrations.
erDiagram
User ||--o{ UserTenant : has
Tenant ||--o{ UserTenant : has
Tenant ||--o| OnboardingState : has
Tenant ||--o{ ProvisionJob : has
Tenant ||--o| Subscription : has
User {
string id PK
string email
}
Tenant {
string id PK
string status
}
UserTenant {
string user_id FK
string tenant_id FK
string role
}
OnboardingState {
string tenant_id FK
string user_id FK
string current_step
string company_draft_json
string updated_at
}
ProvisionJob {
string id PK
string tenant_id FK
string trace_id
string status
}
Subscription {
string id PK
string tenant_id FK
string stripe_customer_id
}
3.3 Draft internal APIs (Bearer INTERNAL_API_KEY or authenticated admin user per existing patterns)
| Method | Route (illustrative) | Body or query | Notes |
|---|---|---|---|
| GET | /internal/onboarding-state | user_id or session | Returns step, draft pointer, dashboard_unlocked. |
| PATCH | /internal/onboarding-state | Partial draft, step advance | Validates transitions server-side. |
| POST | /internal/funnel-trace | trace_id, TTL, metadata | Optional; for funnel analytics instead of only client storage. |
Ordering: Add or extend OpenAPI in prego-zuplo only if exposed through the gateway; pure internal CP routes follow CP README.md and How to add an API for anything browser-callable.
Security (planning): Operator and internal flows — JWT lifetimes, Bearer scope for /internal/*, and rotation for long-lived keys are documented in §8 and Tenant and trust boundaries.
4. admin-web — routing guard and API contract
Problem today: getOnboardingDashboardProgress() and related helpers read localStorage and sessionStorage to decide dashboard unlock and resume paths.
Target contract:
- Single read on shell load:
GETonboarding state from CP (via Zuplo or admin BFF) using the signed-in session. Response includescurrent_step,dashboard_unlocked,tenant_id,provision_job_id(if any). - Guards:
AdminShellRouteGuardanduseRequireDashboardUnlockeduse only API response (plus loading and error states). Remove reliance onprego_onboarding_*_doneandprego_tenant_idinlocalStoragefor authorization decisions. - Optimistic UI: Optional client cache with
stale-while-revalidate; on mismatch, server wins. - Files to align in code (future implementation):
apps/admin-web/lib/onboarding-dashboard-progress.ts,apps/admin-web/lib/onboarding-wizard-provision.ts,apps/admin-web/app/page.tsx, layout guards.
5. www — manual approval continuation
Server truth: CP D1 manual_approval_requests already includes continuation_secret, draft_json, OTP hashes, and related columns (migrations 0027, 0028).
Target behavior:
- Continuation: Browser holds at most the opaque token string required to call
POST /api/trial/manual-approval/*(via Zuplo). Prefer passing token in URL query for email links; minimize redundant copies insessionStorage. - Recovery email: Loaded from server draft, not a second source of truth in
prego_manual_approval_trial_emailexcept as a UX cache cleared on success. - Draft recovery flag: Replace with server-side idempotency or draft version checks where possible.
Files (future implementation): apps/www/lib/trial-manual-approval-session.ts, apps/www/components/trial/ManualApprovalVerifyForm.tsx, apps/www/components/trial/TrialEmailGate.tsx.
6. client-web — phased removal of sessionStorage auth helpers
Principle: JWT and server-set cookies are authoritative. sessionStorage email and employee id are compatibility helpers for passcode and cross-step flows.
Phases:
- Audit: List all readers of
prego_auth_email,prego_auth_employee_id,AUTH_PENDING_EMAIL,AUTH_SLUG, etc., inlib/api-client.ts,components/signin-form.tsx, passcode routes. - Prefer
/auth/me: After any OTP or passcode success, refresh user state from API; use React state or context instead of durable sessionStorage where possible. - Passcode routes: Ensure
PREGO_EIDand pending cookies are set by server responses; reduce duplicate sessionStorage reads. - Demo mode: Gate with
NEXT_PUBLIC_DEMO_MODEand server-side demo headers; avoid persistingPREGO_DEMO_MODEin sessionStorage for production decisions. - Provision trace: Keep header forwarding only; correlate with CP job id server-side.
7. ERP — attendance and Assistant UI (Frappe vs local cache)
| Data | Authority | Browser role |
|---|---|---|
| Attendance check-in / out | Frappe DocTypes (business ledger) | Optional offline queue only; sync with conflict policy. |
| Assistant UI chat history and knowledge index | prego_ai and Frappe adapters per product | Production uses server stores; my-ai-* localStorage is mock or dev-only behind flags. |
| Recent searches (UI convenience) | None required | May remain local if not used for billing or compliance. |
Policy: Document in app README that production Assistant UI persistence is server-side; local mock keys are not supported for compliance-heavy tenants unless explicitly allowed.
8. Operator auth — replacing cp_internal_key in sessionStorage
Current: Operator pastes internal API key; stored in sessionStorage as cp_internal_key (React store and legacy HTML).
Risks: XSS, shoulder surfing, no rotation, long-lived secret in browsing context.
Target directions (choose in security review):
- Short-lived session token: CP issues a token after password or SSO; HttpOnly cookie or memory-only script with refresh.
- OAuth or Cloudflare Access in front of operator routes; CP validates identity.
- Separate operator API keys with rotation, scoped permissions, audit log of usage.
Minimum near-term: Document that keys must be rotated if exposed; prefer not storing raw key in sessionStorage for new operator UIs.
9. Optional phase — cookie consent server audit
Trigger: Legal or product requires provable consent history.
Design sketch:
- On consent save, POST anonymized payload to CP:
schema_version,timestamp,choices_hash, optionaluser_idhash. - Append-only table or log sink; no PII in clear text unless required.
Dependency: Product and legal sign-off before implementation.
10. Implementation order (engineering)
- Freeze inventory (§2) and add lint or docs checks if desired.
- Implement CP D1 migrations and internal GET or PATCH for onboarding state (§3).
- Switch admin-web guards to API-first (§4); delete or demote local flags.
- Tighten www manual approval storage (§5).
- client-web auth storage phases (§6).
- ERP and Assistant UI policy in tenant docs (§7).
- Operator auth hardening (§8).
- Consent audit (§9) if approved.
11. Risks and mitigations
- Latency: More API calls on load — use parallel fetch and short TTL cache with revalidation.
- Migration: Existing users with old localStorage — first load overwrites with server state; one-time banner if needed.
- Cross-repo: Any new public route: OpenAPI in prego-zuplo first per How to add an API.
12. Deliverables checklist
| Deliverable | Location |
|---|---|
| Whitelist and blacklist inventory | §2 |
| CP entity and API draft | §3 (includes ERD §3.2) |
| admin-web guard contract | §4 |
| www manual approval direction | §5 |
| client-web phased plan | §6 |
| ERP and Assistant UI boundary | §7 |
| Operator auth review | §8 |
| Consent audit optional phase | §9 |
This document is the planning source of truth for browser storage minimization; implementation tickets should reference section numbers.