Security Architecture
Security Layers
| Layer | System | Controls |
|---|---|---|
| User Authentication | Clerk | SSO, MFA, session management, JWT issuance |
| User Authorization | Backend middleware | Scope-based permission enforcement |
| Org Isolation | Backend + Database RLS | Multi-tenant data separation |
| Service Auth | Prism API Keys | Scoped, expiring machine tokens |
| Transport | Cloudflare | TLS termination, HSTS, WAF |
| Infrastructure | GitHub, Render, Supabase, Infisical | Platform-level access controls |
Authentication
Clerk Integration
Prism uses Clerk for all user authentication:
- SSO — single sign-on, no direct username/password login
- MFA — required for all accounts with production system access
- JWT tokens — short-lived bearer tokens with org context
- Session management — handled entirely by Clerk
JWT Claims
The Clerk JWT contains:
{
"sub": "user_abc123",
"org_id": "org_xyz789",
"org_permissions": [
"org:buildings:read",
"org:buildings:write",
"org:comps:read",
...
]
}
Backend JWT Validation
- Frontend sends
Authorization: Bearer <token>on all API requests - Backend validates JWT signature against Clerk JWKS endpoint (
CLERK_JWKS_URL) - Backend verifies issuer (
CLERK_ISSUER) and optionally audience (CLERK_AUDIENCE) - Backend extracts
sub,org_id, andorg_permissionsfrom claims - Scope normalization:
org:buildings:read→buildings:read
Strict Scope Mode
STRICT_CLERK_SCOPES=true must be set in production:
- When enabled: missing or empty
org_permissions= zero scopes (deny-by-default) - When disabled: missing permissions may fall back to a default grant (development only)
Authorization (RBAC)
Role Definitions
| Role | Description | Scopes |
|---|---|---|
| Viewer | Read-only stakeholders | All *:read scopes |
| Member | Standard operational users | All *:read + *:write + export |
| Admin | Org owners/operators | All member scopes + api_keys:manage, audit:read, export:admin |
Complete Scope List
Read Scopes (Viewer+):
buildings:read,comps:read,tims:read(mapped fromtim:read)tenants:read,owners:read,contacts:read,brokerage_firms:readreports:read,building_parks:read,key_points:read
Write Scopes (Member+):
- All
*:writevariants of the above export— access to data export
Admin Scopes:
api_keys:manage— create, list, revoke API keysaudit:read— access audit logexport:admin— BI export management
Enforcement Points
- Backend middleware (
RequireScope) — checks scope on every protected route - Admin middleware — additional gate for admin-only routes (
/admin/*) - Frontend UI (
AuthGate,AdminRouteGate) — convenience-only, not authoritative - Database RLS — row-level enforcement on all org-scoped tables
Canonical scope definition: go-backend/internal/domain/auth.go
Scope normalization: go-backend/internal/middleware/auth.go
Organization Isolation
Multi-Tenant Model
Prism is a multi-tenant application where each organization's data is fully isolated:
- JWT-based org context — org ID extracted from Clerk JWT claims
- Backend enforcement — org ID passed to all repository queries for org-scoped resources
- Database RLS — Row-Level Security policies enforce isolation at the database layer
Data Scoping
| Scope | Tables | Isolation Method |
|---|---|---|
| Public | buildings, geography, lookups, key_points, labor_blocks | No org filter, authenticated read |
| Org-Scoped | comps, TIMs, owners, tenants, contacts, reports, audit_log, etc. | organization_id column + RLS |
RLS Policy Functions
| Function | Purpose |
|---|---|
can_org_read(org_id) | User belongs to the org |
can_org_write(org_id) | User is an active member of the org |
is_org_admin(org_id) | User has admin role in the org |
Cross-Org Protections
- Triggers enforce org consistency on join tables (e.g.,
tim_comp_org_consistency()ensures TIM and comp belong to same org) - Building overrides are scoped per org
- Export schema provides org-filtered views for BI tools
- API keys are scoped to specific organizations
API Key Authentication (Service-to-Service)
For machine-to-machine access (scripts, integrations, BI tools):
Key Properties
- Organization-scoped — each key belongs to one org
- Scoped permissions — array of allowed scopes
- Expiring — optional
expires_atdate - Revocable —
revoked_atfor immediate invalidation - Hashed storage — raw key shown once at creation, only hash stored
- Admin-only management — requires
api_keys:managescope
Safe View
The api_keys_safe database view excludes key_hash and adds a computed is_active field. Direct table SELECT is revoked for the authenticated role.
Data Protection
Transport Security
- All traffic encrypted via TLS (terminated at Cloudflare edge)
- HSTS enforced
- Backend port
:8080not publicly accessible - CORS allowlists explicit per environment
Data at Rest
- Database hosted on managed Supabase Postgres (encrypted at rest)
- Redis hosted on managed Upstash (encrypted at rest)
- No secrets in source code or git history
Secrets Management
- All secrets in Infisical (centralized, auto-synced to platforms)
VITE_*frontend variables are considered public- No long-lived backend API keys in frontend runtime
- Rotation policy: 90 days for DB/Redis credentials, on-change for auth keys
Data Deletion
- See
docs/security/data-deletion-policy.mdfor data retention and deletion procedures - Audit log is append-only (no deletion)
Security Headers
The backend sets these security headers via SecurityHeaders middleware:
| Header | Value | Purpose |
|---|---|---|
Strict-Transport-Security | max-age=31536000; includeSubDomains | Force HTTPS |
X-Content-Type-Options | nosniff | Prevent MIME sniffing |
X-Frame-Options | DENY | Prevent clickjacking |
X-XSS-Protection | 0 | Disable legacy XSS filter (rely on CSP) |
Referrer-Policy | strict-origin-when-cross-origin | Limit referrer info |
Permissions-Policy | Restrictive | Disable unused browser features |
Rate Limiting
- Algorithm: Token bucket per client IP
- Backend: Redis-backed rate limiter (
middleware/rate_limit_redis.go) - Configuration:
RATE_LIMIT_PER_SECOND,RATE_LIMIT_BURSTenv vars - Cloudflare real IP: When
TRUST_CLOUDFLARE_HEADERS=true, rate limits use the real client IP behind Cloudflare
Request Tracing
InjectRequestIDmiddleware generates a unique request ID per request- Request ID is propagated to:
- Error responses (
request_idfield) - Structured log entries
- Error log table entries
- Sentry error reports
- Error responses (
- Frontend surfaces request ID in user-visible error details
Error Capture & Audit
Error Log
The error_capture middleware writes 4xx/5xx responses to the error_log table:
- Channel, severity, message, HTTP path/method/status
- Request ID, user ID, stack trace
- RLS: insert for org members, select for admins only, no update/delete
Audit Log
The audit middleware writes data mutations to the audit_log table:
- Actor (user ID, org ID), action, target table/record
- Changed fields, old/new payloads
- Network metadata (IP, user agent)
- Append-only: insert allowed, update/delete denied via RLS
Sensitive Field Handling
API key audit logs use an allowlist — only name, scopes, expires_in_days are logged from request bodies. Sensitive fields like key hashes are excluded.
Infrastructure Access Control
| System | Admin | Standard Access |
|---|---|---|
| GitHub | Engineering lead (Owner/Admin) | Active engineers (Write) |
| Infisical | Backend lead / Ops lead | Engineers (read-only prod) |
| Supabase/RDS | Backend lead (break-glass) | API process only (prism_app role) |
| Redis | Backend lead | — |
| Cloudflare | Ops lead | — |
| Sentry | Backend lead | Active engineers (Member) |
| Render | Ops lead | — |
| Clerk Dashboard | Backend lead | — |
Database Role Model
| Role | Privileges | Used By |
|---|---|---|
postgres (superuser) | Full access | Break-glass only |
prism_app | USAGE on public, CRUD via RLS | API process |
authenticated | Read public, org-scoped CRUD via RLS | Supabase auth context |
service_role | Execute refresh/snapshot functions | Scheduled jobs, export refresh |
The prism_app role has no SUPERUSER and no BYPASSRLS — all access goes through RLS policies.
Incident Response
See docs/security/incident-response.md for full procedures.
Severity Levels
| Severity | Response Target | Example |
|---|---|---|
| P1 — Critical | < 15 min | Production down, confirmed data breach |
| P2 — High | < 1 hour | Elevated 5xx, auth failures, critical CVE |
| P3 — Medium | < 4 hours | Single endpoint issue, staging problem |
| P4 — Low | Next business day | Non-critical dep alert, doc drift |
Detection Sources
- Sentry alerts (error spikes, performance degradation)
- Render service metrics (CPU, memory, restarts)
- GitHub secret scanning / Dependabot alerts
- External reports
- Manual observation
Vulnerability Management
See docs/security/vulnerability-management.md for full procedures.
- Dependabot enabled for Go modules and npm packages
- CodeQL static analysis in CI
- Critical/high vulnerabilities addressed within SLA
- Regular dependency updates
Related Documents
- Access control policy:
docs/security/access-control-policy.md - Secrets management:
docs/security/secrets-management.md - Incident response:
docs/security/incident-response.md - Vulnerability management:
docs/security/vulnerability-management.md - Backup and DR:
docs/security/backup-restore-and-dr.md - Data deletion:
docs/security/data-deletion-policy.md - Backend security audit:
docs/security/security-audit-2026-04-01.md