MVP implemented, feat/real-saas-foundation (2026-08-09). This replaces the
pre-implementation checklist that was here before — see
docs/MVP_VERIFICATION.md for full evidence of what's tested vs. not.
FIREBASE_PRIVATE_KEY,OPENAI_API_KEY,FIREBASE_CLIENT_EMAILare server-only — neverVITE_-prefixed, never imported by anything undersrc/pagesorsrc/components.src/lib/firebase/admin.tsthrows if accidentally imported into browser code (checked viaglobalThis, not the barewindowidentifier, so it type-checks under both the DOM and DOM-less TS projects — see the file's comments).VITE_FIREBASE_*client config is intentionally public — Firebase's own security model relies on Firestore Security Rules and Auth, not on hiding that config. See.env.example.- No secret value has been printed, logged, or committed at any point in
this build — verified by grepping for
localStorage+ auth/token patterns, hardcoded secrets, andVITE_*_API_KEY/VITE_*_SECRETclient exposure acrosssrc/andapi/: no hits beyond expected placeholders.
See docs/AUTHORIZATION.md for the full server flow and route matrix.
Summary: Firebase ID token → Admin SDK verifyIdToken → workspaceMembers
lookup → role check, fail-closed at every step. No localStorage auth
bypass, no demo-admin bypass, no email-based admin check anywhere in the
codebase.
Two independent layers:
- Structural — every workspace-scoped query is built from
workspaces/{workspaceId}/..., whereworkspaceIdcomes from a server-verified source (the path param checked againstworkspaceMembers, or thepublicWidgetKeyresolution inapi/chat.ts) — never from unchecked client input. There is no code path that can query across workspaces. - Firestore Security Rules (
firebase/firestore.rules) — defense in depth in case a client ever queries Firestore directly with a malformed or forged path. Rules tests (src/test/firestoreRules.rules.test.ts) prove Workspace A cannot read Workspace B's workspace doc, leads, or another user'susers/{uid}profile. This local sandbox's Java 8 is too old for the Firestore emulator (needs 21+), but therules-testsCI job (real JDK 21) ran them for real on PR #7: 7/7 passed — seedocs/LOCAL_DEVELOPMENT.mdfor the local setup anddocs/MVP_VERIFICATION.mdfor the CI run link.
api/chat.ts — deny by default. An unrecognized Origin gets no
Access-Control-Allow-Origin header (src/lib/http/originPolicy.ts), never
falls back to a trusted default. See docs/WIDGET.md.
Every write path (api/workspaces/*) validates its body with a Zod schema
before touching Firestore (src/lib/validation/) — 400 with a structured
error, never a raw exception, on invalid input.
src/lib/analytics/track.ts — property allow-listing (only
source/intent/status/channel/durationMs/reason/count are ever stored; raw
messages, emails, and phone numbers are silently dropped even if passed in)
and fail-closed workspace attribution (empty/missing workspaceId → event
rejected, never attributed to a fallback bucket). Both behaviors are
directly unit tested (src/lib/analytics/track.test.ts).
See docs/AI_ARCHITECTURE.md — deterministic security pre-router (before
any model call), structured-output validation, post-generation policy
checks, and safe fallback on every failure mode. All unit tested with
dependency-injected fake model responses (no live OpenAI key required for
this test coverage).
Known simplifications (honest, not hidden)
- Any active workspace member can read draft knowledge in the dashboard
(not just approved). Only
status == "approved"is ever sent to the AI model — the simplification is about dashboard visibility, not what the AI can say. - No rate limiting on the authenticated dashboard API routes (
leads,knowledge, etc.) — only the public/api/chatendpoint is rate limited. Reasonable for an MVP (these routes require a real Firebase session), but a gap worth closing before wide pilot traffic. - Dependency vulnerabilities:
npm auditreports vulnerabilities in the installed tree (mostly fromfirebase-tools, a devDependency used only for local emulator/rules testing, not shipped to production). Not triaged in this pass — flagged for follow-up before a real pilot.