The invariants we won't let break.
Six surfaces, each with named files, named guardrails, and a frank note on what's still ahead. We publish this because dealerships should be able to verify, not just trust.
Strict per-owner_id keying. No fallbacks.
Every customer-facing data row belongs to one dealership, keyed by owner_id (uuid → auth.users(id)). Every query filters by it. Supabase RLS policies enforce server-side; service-role calls in webhooks and trigger tasks add explicit filters.
The lead-lookup helper keys by ONE of email or phone — never both. Two customers sharing a household landline do not get merged. This is documented as a non-obvious invariant; refactors that 'simplify' it to OR-matching are a regression.
AI prompts assemble dynamically from the dealership profile loaded via lead.owner_id. There are no hardcoded brand strings in prompt modules; a code reviewer rejects PRs that reintroduce them.
supabase-actions/leads/getLeadByEmailorPhone.ts · db/rls-policies.sql · lib/prompts/*.tsSix guardrails. Each tied to an eval failure.
Every directive in lib/prompts/emailSystemPrompt.ts has a comment citing the eval-baseline failure that motivated it: no APR quotes (edge_financing_specifics, helpfulness=2), no trade-in dollar quotes (inv_trade_in_valuation, scored 3), brand deflection (inv_non_toyota_request, scored 1), only-facts-provided (groundedness was the weakest judge dimension).
Lead-field interpolations HTML-escape via lib/text/escapeLeadField before render — defense against template injection from user-controlled lead names.
Inventory mentions go through pickPrimaryItem with a confidence threshold; below threshold the smart-component is dropped, not invented.
Post-generation finalize chain: stripEmDashes → stripUngroundedClaims → HTML sanitizer. The persisted message body is the cleaned output, not the model's raw text.
SMS keyword compliance: STOP/START/HELP classified and acked via inline TwiML, bypassing the suppression list so a STOP recipient always receives confirmation.
lib/prompts/{email,chat,sms,voice}SystemPrompt.ts · lib/text/* · lib/inventory/pickPrimaryItem.ts · lib/compliance/sms.tsConstant-time secret compare. Production rejects unsigned.
Twilio webhooks (SMS + voice): HMAC-SHA1 of (URL + sorted params) with TWILIO_AUTH_TOKEN. Production REQUIRES the signature; missing or invalid is 403. Was a P0 bypass in the audit; now enforced.
Widget chat: no HMAC — identity is (public_key, sessionToken). Per-widget allowed_origins[] is the abuse gate; check fails → request rejected.
personaplex-service WebSocket: when BRIDGE_SECRET is set, every /ws/twilio/* connection must present a token + exp signed by the Next.js TwiML route.
app/api/webhooks/email/route.ts · utils/webhooks/verify.ts · personaplex-service/server.py · lib/voice/personaplexAuth.tsTwo tiers. Every API gated.
Middleware-level (middleware.js): 5/min auth+admin, 20/min LLM, 15/min upload, 60/min standard. Excludes /api/webhooks, /api/twilio, /api/campaigns, /api/ai-agents/test-plugins (those bypass middleware and have their own per-route limit).
Per-route (lib/security/rate-limiter.ts withRateLimit): used by webhooks at 100/min, allows opt-in higher granularity.
Both limiters are in-memory today; the Redis backend interface is defined but not yet wired. Multi-instance hosting can multiply limits — tracked as a known production-readiness gap.
On exceed: 429 with Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers.
middleware.js · lib/security/rate-limiter.tsStrict by default. Per-path overrides for the widget.
Default: X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Strict-Transport-Security 2-year + preload, Permissions-Policy locking down camera / microphone / geolocation.
Default CSP: script-src self + Stripe + GTM, connect-src self + supabase + plenfi + openai + twilio + sendgrid, frame-ancestors none.
Widget paths (/widget.js, /widget/*, /api/widget/*) get a permissive override allowing frame-ancestors * and wide CORS so the loader can be embedded; per-widget Origin allow-list is the abuse gate.
middleware.js → securityHeaders + isWidgetPathWhat is in scope and what isn't — honestly.
In scope: GDPR, CCPA, basic web security (OWASP), SMS keyword compliance (STOP/START/HELP via TwiML).
Not in scope today: SOC 2, HIPAA, PCI. The product handles name / contact / vehicle interest only — no financial details, no health, no card numbers.
Dealership-side compliance: A2P 10DLC for SMS (US carrier compliance) is the dealership's responsibility today; the product doesn't enforce or prompt for registration. CASL (Canada) and TCPA (US) consent capture is implicit.
Audit reports live in the repo: AI_HALLUCINATION_AUDIT_REPORT.md, SECURITY_AUDIT_REPORT.md, AUDIT_REPORT.md, SANDBOX_EVALUATION_REPORT.md.
AI_HALLUCINATION_AUDIT_REPORT.md · SECURITY_AUDIT_REPORT.mdThey're in the repo. We'll happily walk you through them.
Pilot dealerships get read-only access to the reports referenced here, plus the engineering-reference product.md with file-by-file detail.