Skip to main content

Architecture and data flow

This page describes how the Better Comply components connect, what security controls sit at each boundary, and where regulated evidence is persisted.

Who this is for

Operators who need to understand the system before configuring or auditing a deployment.

Component layout

Browser (React SPA on Netlify CDN)

│ HTTPS - read queries (RLS-gated)

Supabase
├── Auth (GoTrue) JWT issuance and refresh
├── Postgres + pgvector All relational data + vector search
└── Object Storage Certificates, document blobs, quiz images

│ HTTPS - POST /v1/<route> state changes and AI operations

Cloud Run (Fastify / Node 20)
├── auth middleware Verifies JWT, re-reads role from user_roles
├── /v1/generate-content AI content, admin-only
├── /v1/generate-quiz AI quiz generation
├── /v1/process-document RAG indexing trigger
├── /v1/recertification-scan Secret-gated cron
├── /v1/supervisor-report-scan Secret-gated cron
└── ... (all other routes)

│ service-role key or caller-scoped JWT

Supabase (same project - Cloud Run reads/writes back)
Screenshot pendingnetwork diagram showing browser to Supabase and browser to Cloud Run paths

Request paths

Browser to Supabase (direct)

Read-only SELECT queries run from the browser through the Supabase JS client using the user's JWT. Row Level Security (RLS) policies on every table ensure each user sees only the data their role permits.

The browser never writes directly to regulated tables (evidence, training_versions, audit_logs, document_versions, document_approvals). Those writes go through Cloud Run routes that validate input, enforce authorization, and emit fail-loud audit events before touching the database.

Browser to Cloud Run

The browser calls POST /v1/<route> via the invokeBackendFunction helper (packages/web/src/lib/backendApi.ts). Every call forwards the user's Supabase session token as Authorization: Bearer <access_token>.

The Cloud Run backend:

  1. Verifies the JWT with Supabase Auth.
  2. Re-reads the caller's role from user_roles (never trusts the role embedded in the JWT, which can lag behind a role change).
  3. Enforces per-route authorization rules (admin-only, owner-or-admin, etc.).
  4. Performs the operation.
  5. Emits an audit event via logAuditEvent before returning the response. For fail-loud actions, a failed audit RPC aborts the whole operation.

Cloud Run to Supabase

The backend uses the Supabase service-role key for administrative writes (certificate storage, seed operations, cron drains) and the caller's forwarded JWT for RAG retrieval - so that auth.uid() resolves inside the database function and the caller's department-level filter is applied (defense-in-depth against cross-tenant data access).

Security headers (Netlify)

All responses from Netlify include these headers, set in netlify.toml:

HeaderValue
Content-Security-Policydefault-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' https://*.supabase.co https://*.supabase.in wss://*.supabase.co; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-ancestors 'none'; form-action 'self'; base-uri 'self'; object-src 'none'
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preload
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policygeolocation=(), microphone=(), camera=(), payment=()
Cross-Origin-Opener-Policysame-origin

style-src 'unsafe-inline' is intentional: Tailwind and shadcn/ui inject runtime style blocks. script-src is locked to 'self' with no unsafe-inline or unsafe-eval.

The Cloud Run backend uses @fastify/helmet for basic security defaults. CSP is the frontend's responsibility; the backend serves JSON and PDF only.

Where regulated data lives

TableWhat it storesKey invariants
audit_logsEvery logAuditEvent call from both browser and backendAppend-only; no UPDATE or DELETE policy; SECURITY DEFINER RPC
evidenceLearner completion records (signed or unsigned)Append-only; signed_ip written server-side by the record_training_evidence RPC from inet_client_addr()
training_versionsVersioned training content, quiz, objectivesImmutability trigger once evidence references the version; DELETE blocked
document_approvalsE-signature records for controlled documentsAppend-only; signed_ip from inet_client_addr() inside the record_document_approval RPC
document_versionsVersioned document content and lifecycle stateRequires edit_reason >= 10 chars; reason cannot be cleared after insert
training_version_sourcesSource attachments for training versionsAppend-only; DELETE blocked

CORS

CORS is configured on the Cloud Run backend via @fastify/cors. The allowed origins come from the ALLOWED_ORIGIN environment variable (comma-separated). An empty value falls back to * for local development only. In production you must set ALLOWED_ORIGIN to the exact frontend origin (e.g. https://app.better-comply.example).

Health probes

Cloud Run uses two HTTP probes:

  • GET /healthz - liveness: returns 200 when the process is up.
  • GET /readyz - readiness: returns 200 when the service can accept traffic.

Neither probe requires authentication.