Enterprise mode uses Supabase Auth. public.users is the role-backed profile table and references auth.users. public.memberships maps staff users into workspaces. In production mode, email_confirmed_at is required before admin pages, workspace creation, portal identity binding, billing actions, or invitation acceptance can proceed. The /admin workspace is protected by proxy.ts when Supabase env vars are configured, with the server-side verification guard in lib/auth/page.ts and lib/auth/api.ts as the final boundary.
The active workspace is stored as an opaque workspace ID in the HTTP-only supportpilot_active_workspace cookie. The cookie is same-site, secure in production, and never grants access by itself: proxy and server authorization revalidate the selected workspace against the caller's active membership. Sign-out removes the cookie.
Hosted SAML remains disabled at the workspace layer until a configured domain also appears in deployment-controlled SUPPORTPILOT_SAML_VERIFIED_DOMAINS. The auth callback performs JIT mapping only for Supabase SSO/SAML identities, rejects duplicate domain ownership, and limits automatic roles to agent, analyst, or viewer. Owner/admin/manager access is never granted through JIT. Workspace owners/admins can record SSO state, break-glass ownership, and periodic access reviews in the Settings security panel; provider configuration and live IdP rehearsal remain separate launch evidence.
customer: can read owned customer/ticket/message rows and create owned tickets/messages.support_agent: can triage tickets, draft AI replies, approve normal-risk drafts, and write support messages.support_manager: can review escalated/high-risk drafts and manage escalation decisions.admin: can manage staff profile roles, audit visibility, docs, and rules.- workspace membership roles:
owner,admin,manager,agent,analyst, andviewerscope staff access to a workspace.
The migrations enable RLS on every enterprise table. Policies separate customer-owned access from support staff access, then scope staff access through public.can_access_workspace() and the owner/admin-only public.can_administer_workspace() helper added in migration 016_role_policy_hardening.sql. Agents can operate tickets/messages, managers can review, and owner/admin roles alone can mutate sources, ingestion/embedding jobs, integrations, workspace settings, and onboarding state. Profile inserts default to customer and a database trigger prevents self-escalating the legacy profile role. Service-role operations are limited to server code in lib/db/support.ts and must never be exposed to the browser.
Widget-facing routes also enforce workspace_domains origin checks. A script installed on an unverified host cannot fetch widget config, create a widget session, post chat messages, or submit feedback for that workspace. Cross-origin config/session requests use explicit allowlisted CORS responses and preflight handling; the iframe carries the approved host origin into signed session validation. Feedback is bound to the persisted aiRunId workspace instead of trusting a client workspace ID. Portal APIs and /portal resolve workspace access through an active portal identity, the configured first-time portal workspace, or an active membership; they do not trust arbitrary client workspace IDs.
Escalation email authorization follows the canonical workspace on a supplied ticket and rejects a mismatched client workspace before sending or recording the escalation.
Custom domains are not trusted when they are first added. Owners/admins receive a _supportpilot.<domain> TXT/CNAME challenge, and /api/workspaces/[workspaceId]/domains/[domainId]/verify must observe the expected DNS record before the domain becomes an allowed widget origin. Verification attempts are timestamped and audit logged. The bulk recheck endpoint can run under owner/admin auth or SUPPORTPILOT_DOMAIN_RECHECK_SECRET for cron-style DNS health checks.
When SUPPORTPILOT_WIDGET_SESSION_SECRET is configured, POST /api/widget/session issues a short-lived HMAC-signed session token for a verified origin. /api/chat and /api/feedback validate that token before accepting widget traffic or answer feedback. Without the secret, local/demo mode keeps the older origin-allowlist behavior.
Required production secrets:
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY- one provider key for
LLM_PROVIDER - optional
SENTRY_DSN - optional
RESEND_API_KEY - optional
NEXT_PUBLIC_POSTHOG_KEY - optional
SUPPORTPILOT_WIDGET_SESSION_SECRET - optional
SUPPORTPILOT_DOMAIN_CNAME_TARGET - optional
SUPPORTPILOT_DOMAIN_RECHECK_SECRET - optional
SUPPORTPILOT_DOMAIN_STALE_DAYS - optional
SUPPORTPILOT_INGESTION_WORKER_SECRET - optional
SUPPORTPILOT_KNOWLEDGE_SOURCE_BUCKET - optional
SUPPORTPILOT_RETENTION_WORKER_SECRET - optional
SUPPORTPILOT_AUDIT_EVIDENCE_BUCKET - optional
SUPPORTPILOT_EVAL_WORKER_SECRET - optional
UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN - optional
SUPPORTPILOT_ALLOW_MEMORY_RATE_LIMITSfor controlled non-Redis production exercises only - optional
EMBEDDING_PROVIDER, provider embedding keys/models, andSUPPORTPILOT_ALLOW_DETERMINISTIC_EMBEDDINGSfor controlled deterministic production exercises only - optional
LOCAL_MODEL_ENDPOINT,LOCAL_EMBEDDING_ENDPOINT,LOCAL_RERANKER_ENDPOINTand their bearer-token/timeout settings
SUPABASE_SERVICE_ROLE_KEY, provider keys, Resend keys, invitation sender addresses, and Sentry auth tokens belong only in server-side deployment env vars.
Integration account credentials use the write-only API flow. In Supabase deployments, migration 020_workspace_selection_vault_secrets.sql stores raw values in Supabase Vault and application rows retain only opaque secret references plus rotation/use timestamps. Vault RPCs are revoked from public, anonymous, and authenticated roles and granted only to service_role. API reads never return a secret or reference, and production rejects plaintext access/API/signing secrets embedded in integration configuration.
Required identity deployment controls:
SUPPORTPILOT_SAML_VERIFIED_DOMAINS: comma-separated domains that exist in hosted Supabase SAML and have passed ownership/routing verification.SUPPORTPILOT_SAML_REHEARSED_AT: timestamp set only after a successful IdP login, safe-role mapping, MFA, sign-out, and break-glass rehearsal.
Public chat, widget config, widget session, and knowledge-upload routes use scoped rate limits before expensive or tenant-sensitive work begins. Supabase-backed production deployments must configure Upstash Redis REST for a persistent shared limit store. If Redis is missing or unhealthy in production, the limiter returns a blocked unconfigured result instead of falling back to process memory. Local demos and tests keep the memory limiter; SUPPORTPILOT_ALLOW_MEMORY_RATE_LIMITS=true is reserved for explicitly controlled production exercises and should not be used for public launch traffic.
Supabase-backed knowledge ingestion stores uploaded source bytes in the private supportpilot-knowledge-sources Storage bucket before queued extraction. The database job payload keeps a supabase://bucket/path reference, filename, and byte count instead of raw base64 content when storage succeeds. Workers read sources through the server-side service-role client; browser code never receives the private object path as a public URL.
Production embeddings must use a managed provider (local, openai, or google) with ready credentials. Deterministic hash embeddings remain available for local demos, tests, and explicitly controlled production exercises through SUPPORTPILOT_ALLOW_DETERMINISTIC_EMBEDDINGS=true, but they are blocked by default in production mode. Model routing is tenant-controlled through the persisted workspaces.model_policy field, defaults to managed_only, and only permits local generation when an owner or admin has selected an explicit policy in Settings.
Workspace retention settings schedule conversation and AI-log cleanup jobs. Processed jobs redact aged ticket subjects, messages, escalation details, AI prompts, AI responses, rationales, model-route task text, and model-route reasons while preserving non-PII operational metadata, counts, timestamps, costs, confidence, and proof hashes for auditability.
Verified deletion requests create scoped jobs for tickets, customers, or source documents. Ticket/customer requests anonymize personal support content and customer identity fields without deleting the audit trail. Source-document requests delete the approved source and its vector chunks. POST /api/security/retention/jobs/run is worker-secret protected and can drain queued jobs for an external scheduler when SUPPORTPILOT_RETENTION_WORKER_SECRET is configured.
Audit evidence exports write a non-PII manifest hash to audit_evidence_exports, chain successful exports through previousArtifactHash and evidenceChainHash, and audit the chain metadata. Supabase-backed deployments also upload the JSON artifact to the private supportpilot-audit-evidence Storage bucket, or the bucket named by SUPPORTPILOT_AUDIT_EVIDENCE_BUCKET, and store only a supabase://bucket/path reference in the database. Workspace members can read artifacts through RLS-scoped storage policies; owner/admin/manager roles can write them. Local demos keep a memory://audit-evidence/... artifact reference. Provider-native retention lock or Object Lock rehearsal remains required before claiming WORM storage.
Scheduled golden-question evals are worker-secret protected through SUPPORTPILOT_EVAL_WORKER_SECRET. Runs store pass/fail counts, per-case metadata, and a SHA-256 artifact hash in golden_eval_runs; they also update the canonical golden_questions score/pass state and write an audit log. This gives launch readiness checks repeatable evidence without storing raw customer prompts.
- AI drafts are saved as
ai_runs; they are not customer-visible until approved or edited. - High-risk drafts require manager review.
- Prompts instruct the model to answer from retrieved approved context and escalate low-confidence cases.
- Risk flags cover low confidence, angry sentiment, legal/policy, billing/refund, and sensitive data.
- Prompt logging is redacted by default. The app stores prompt hashes, redacted previews, route metadata, token estimates, latency, and cost estimates instead of raw private content.
- The model router selects deterministic R0-R5 routes before generation and logs every route in
model_route_logs. - Policy decisions are stored in
policy_evaluations; citation coverage and source freshness are stored ingrounding_checks. - Only read-only agent tools are scaffolded in this pass. Refunds, account changes, customer-reply email, and external system writes remain approval-gated future work.
- Every draft and decision writes an audit event.
- Usage events track chat, approval, knowledge upload, and escalation activity without storing raw secrets.
The app can send invitation and escalation email through Resend, but it does not yet sync approved replies into an external helpdesk. Add Zendesk, Intercom, Slack, or CRM connectors only after final customer-copy review and audit logging succeed.
Local controls do not prove hosted SAML, Supabase Vault, RLS, provider retention lock, or backup behavior. Those claims require the provider-side records listed in SOC2_READINESS_EVIDENCE.md; until attached, the system remains controlled-pilot incomplete.
Outbound Slack and generic webhook endpoints must use public https URLs. Setup and delivery reject localhost, private-network, and link-local targets before sending requests, which keeps integration delivery from becoming an internal network fetch path.
GET /api/health exposes only pass/warn/fail readiness labels for production dependencies. It does not return secret values, tenant data, customer content, prompts, or workspace records.