Property PrismDev Hub

Application Security — Comprehensive Reference

Complete application security reference covering authentication, authorization, encryption, data protection, org isolation, middleware stack, data retention, and operational security processes.

Updated Apr 3, 2026

Table of Contents

  1. Security Architecture Overview
  2. Authentication
  3. Authorization (RBAC)
  4. Organization Isolation
  5. Encryption & Data Protection
  6. Middleware Security Stack
  7. Input Validation & Request Limits
  8. Rate Limiting
  9. Security Headers
  10. CORS Policy
  11. Audit Trail
  12. Error Handling & Monitoring
  13. Data Retention & Deletion
  14. Secrets Management
  15. Infrastructure Security
  16. Vulnerability Management
  17. Incident Response
  18. Backup & Disaster Recovery

1. Security Architecture Overview

Prism implements defense-in-depth with security controls at every layer of the stack:

┌─────────────────────────────────────────────────────────┐
│                    Client (Browser)                      │
│  Clerk.js SDK → JWT acquisition, session management      │
└───────────────────────┬─────────────────────────────────┘
                        │ HTTPS (TLS 1.2+)
┌───────────────────────▼─────────────────────────────────┐
│              Cloudflare Edge (TLS termination)           │
│  WAF · DDoS mitigation · HSTS · Bot management          │
│  CF-Connecting-IP header injection                       │
└───────────────────────┬─────────────────────────────────┘
                        │ Internal (Cloudflare Tunnel)
┌───────────────────────▼─────────────────────────────────┐
│               Go Backend (Chi Router)                    │
│                                                          │
│  ┌─ Middleware Stack (13 layers) ───────────────────┐   │
│  │  1. Sentry panic capture                         │   │
│  │  2. Chi Recoverer                                │   │
│  │  3. Gzip compression                             │   │
│  │  4. CORS enforcement                             │   │
│  │  5. Structured logging                           │   │
│  │  6. Prometheus metrics                           │   │
│  │  7. Audit logging (mutation tracking)            │   │
│  │  8. Request body size limits                     │   │
│  │  9. Security headers                             │   │
│  │ 10. Rate limiting (Redis / in-memory)            │   │
│  │ 11. Real IP extraction (CF-Connecting-IP)        │   │
│  │ 12. Request ID injection                         │   │
│  │ 13. Auth (Clerk JWT / API Key) + Scope check     │   │
│  └──────────────────────────────────────────────────┘   │
│                                                          │
│  Handler → Service → Repository → Database               │
└───────────────────────┬─────────────────────────────────┘
                        │ TLS (connection pooling via pgx)
┌───────────────────────▼─────────────────────────────────┐
│            PostgreSQL 15 + PostGIS                       │
│  Row-Level Security (RLS) on all org-scoped tables       │
│  Org consistency triggers on join tables                  │
│  Encrypted at rest (managed platform)                    │
└─────────────────────────────────────────────────────────┘

Security Layers Summary

LayerSystemControls
Edge / TransportCloudflareTLS 1.2+ termination, WAF, DDoS, HSTS, bot mitigation
User AuthenticationClerkSSO, MFA, session management, JWT issuance
Service AuthenticationPrism API KeysSHA-256 hashed, scoped, expiring machine tokens
AuthorizationBackend middleware30+ scope-based permissions with role bundles
Org IsolationBackend + Database RLSMulti-tenant data separation at every layer
Data ProtectionPlatform encryptionEncrypted at rest (Supabase Postgres, Upstash Redis)
SecretsInfisicalCentralized secret management, auto-sync to platforms
ObservabilitySentry + PrometheusError capture, metrics, request tracing

2. Authentication

Prism supports two authentication methods — Clerk JWT (user sessions) and API Keys (machine-to-machine). Every API request must authenticate via one of these methods.

2.1 Clerk JWT Authentication (User Sessions)

Flow:

  1. User authenticates via Clerk.js SDK in the browser (SSO, social login, or email — MFA required for production access)
  2. Clerk issues a short-lived JWT containing user identity, org context, and permissions
  3. Frontend sends Authorization: Bearer <token> on every API request
  4. Backend validates the JWT and extracts auth context

Implementation: go-backend/internal/middleware/auth.go, go-backend/internal/auth/clerk.go

JWT Validation Process

The ClerkValidator performs multi-step validation:

Token received
  │
  ├─ 1. Parse JWT header → extract `kid` (key ID)
  │
  ├─ 2. Fetch signing key from JWKS endpoint
  │     └─ Cached with 1-hour TTL (background refresh)
  │     └─ SSRF protection on JWKS fetch (see §2.3)
  │
  ├─ 3. Verify RSA signature (RS256 / RS384 / RS512 only)
  │     └─ HMAC algorithms explicitly rejected
  │
  ├─ 4. Validate standard claims:
  │     ├─ `exp` — must be present and not expired
  │     ├─ `iss` — must match configured CLERK_ISSUER
  │     └─ `aud` — must match CLERK_AUDIENCE (if configured)
  │
  └─ 5. Extract Prism-specific claims:
        ├─ `sub` → user ID
        ├─ `org_id` → organization ID
        └─ `org_permissions` → permission array

JWT Claims Structure

{
  "sub": "user_abc123",
  "org_id": "org_xyz789",
  "org_permissions": [
    "org:buildings:read",
    "org:buildings:write",
    "org:comps:read",
    "org:tims:read",
    "org:tims:write"
  ],
  "iss": "https://clerk.property-prism.com",
  "aud": "prism-api",
  "exp": 1712000000,
  "iat": 1711996400
}

Scope Normalization

Clerk permissions use org: prefix format. The backend normalizes these to internal scope format:

  • org:buildings:readbuildings:read
  • org:tims:writetims:write
  • org:api_keys:manageapi_keys:manage

Strict Scope Mode

STRICT_CLERK_SCOPES=true must be set in production.

ModeBehavior
Strict (production)Missing or empty org_permissions = zero scopes. Deny-by-default.
Relaxed (dev only)Missing permissions may fall back to default grant for development convenience.

Wildcard Scope Detection

If a JWT contains a wildcard scope (*), the middleware:

  • Logs a structured error with the user ID and org ID
  • Does not grant elevated access — wildcard scopes are not honored

2.2 API Key Authentication (Machine-to-Machine)

For scripts, integrations, and BI tools that cannot use browser-based JWT auth.

Flow:

  1. Org admin creates an API key via POST /api/v1/api-keys (requires api_keys:manage scope)
  2. Backend generates a cryptographically random key, stores only the SHA-256 hash
  3. Raw key is returned once in the creation response — never stored or retrievable again
  4. Client sends the raw key via X-API-Key header on subsequent requests
  5. Backend hashes the provided key with SHA-256, looks up the hash in the database

Implementation: go-backend/internal/middleware/auth.go

API Key Validation Process

X-API-Key header received
  │
  ├─ 1. Length check: reject if > 512 bytes (guard against abuse)
  │
  ├─ 2. SHA-256 hash the raw key
  │
  ├─ 3. Database lookup by hash
  │     └─ Returns: key record with scopes, org_id, expires_at, revoked_at
  │
  ├─ 4. Expiration check: reject if past expires_at
  │
  ├─ 5. Revocation check: reject if revoked_at is set
  │
  ├─ 6. Org active check: verify the org is still active
  │
  ├─ 7. Set auth context (org ID, scopes, auth method = "api_key")
  │
  └─ 8. Async update of last_used_at timestamp (non-blocking)

API Key Security Properties

PropertyDetail
StorageSHA-256 one-way hash only — raw key never stored
Scope restrictionsapi_keys:manage and operations:write are excluded from API key delegation (admin-only scopes cannot be delegated)
Organization-scopedEach key belongs to exactly one org
ExpiringOptional expires_at date
RevocableImmediate invalidation via revoked_at timestamp
Safe viewapi_keys_safe database view excludes key_hash column; direct table SELECT is revoked for the authenticated role

Scope Delegation Validation

The ValidAPIKeyScopes allowlist explicitly defines which scopes can be assigned to API keys. This prevents privilege escalation — an admin cannot create an API key with api_keys:manage scope, which would allow the key to create more keys.

Excluded from API key delegation:

  • api_keys:manage
  • operations:write

2.3 SSRF Protection (JWKS Fetch)

The JWKS endpoint fetch includes multiple SSRF defenses:

DefenseImplementation
HTTPS onlyRejects http:// URLs
IP rejectionBlocks private IPs (10.*, 172.16-31.*, 192.168.*), loopback (127.*, ::1), link-local
Localhost rejectionBlocks localhost hostname
Hostname allowlistOnly fetches from .clerk.accounts.dev, .clerk.com, api.clerk.com, property-prism.com
Redirect limitMaximum 3 redirects, each must be same-host
Custom HTTP clientUses CheckRedirect function with same SSRF checks on each redirect

Configuration:

var defaultJWKSAllowedHostSuffixes = []string{
    ".clerk.accounts.dev", ".clerk.com", "clerk.accounts.dev",
    "clerk.com", "api.clerk.com", "property-prism.com",
}

2.4 JWKS Key Caching

ParameterValue
Cache TTL1 hour
RefreshBackground refresh before expiry
Key typesRSA only (RS256, RS384, RS512)
HMAC rejectionExplicitly rejected to prevent algorithm confusion attacks

3. Authorization (RBAC)

3.1 Role Definitions

RoleDescriptionScope Set
ViewerRead-only stakeholdersAll *:read scopes
MemberStandard operational usersAll *:read + *:write + export
AdminOrg administratorsAll member scopes + api_keys:manage, audit:read, export:admin

3.2 Complete Scope Inventory

Defined in go-backend/internal/domain/auth.go:

Read Scopes (Viewer+):

ScopeResource
buildings:readBuilding records
comps:readComp lease records
tims:readTIM (Tenant-in-Market) records
tenants:readTenant lookup records
owners:readOwner lookup records
contacts:readContact records
brokerage_firms:readBrokerage firm records
reports:readReport records
building_parks:readBuilding park records
key_points:readKey point (port/airport/interchange) records

Write Scopes (Member+):

ScopeResource
buildings:writeCreate/update/delete buildings
comps:writeCreate/update/delete comps
tims:writeCreate/update/delete TIMs
tenants:writeCreate/update/delete tenants
owners:writeCreate/update/delete owners
contacts:writeCreate/update/delete contacts
brokerage_firms:writeCreate/update/delete brokerage firms
reports:writeCreate/update/delete reports
building_parks:writeCreate/update/delete building parks
key_points:writeCreate/update/delete key points
exportData export access

Admin Scopes:

ScopeResource
api_keys:manageCreate, list, revoke API keys
audit:readView audit log
export:adminBI export management (credential lifecycle)
operations:writeOperations queue state management

3.3 Enforcement Points

Authorization is enforced at four levels:

  1. Route-level middleware (RequireScope) — checks the auth context has the required scope before the handler executes. Applied to every protected route group.

  2. Admin middleware — additional gate for /admin/* routes that requires admin-level scopes.

  3. Frontend UI gates (AuthGate, AdminRouteGate) — hides UI elements the user cannot access. These are convenience-only, not authoritative — the backend enforces the real check.

  4. Database RLS — row-level policies enforce org isolation even if application-layer checks were somehow bypassed (defense-in-depth).


4. Organization Isolation

4.1 Multi-Tenant Architecture

Prism is a multi-tenant SaaS application. Each organization's data is fully isolated through a layered enforcement model:

JWT claims (org_id)
  │
  ├─ Backend middleware extracts org_id → sets auth context
  │
  ├─ Service layer passes org_id to all repository calls
  │
  ├─ Repository layer includes org_id in all SQL WHERE clauses
  │
  └─ Database RLS enforces org_id check at the row level

4.2 Data Classification

ClassificationTablesIsolation
Publicbuildings, geography_*, lookups_*, key_points, labor_block_groupsNo org filter; requires authentication but visible to all orgs
Org-Scopedcomps, tims, contacts, owners, tenants, brokerage_firms, reports, audit_log, api_keys, import_jobs, etc.organization_id column + RLS policies
Systemorganizations, user_org_membershipsRLS policies based on membership

4.3 Row-Level Security (RLS) Implementation

All tables in public schema have RLS enabled, including public datasets (defense-in-depth).

RLS Helper Functions

Defined in database/PRISM_vNext_FINAL.sql:

FunctionPurposeUsed By
current_user_id_text()Returns the current session's user IDAll RLS policies
can_org_read(org_id)User is a member of the specified orgSELECT policies
can_org_write(org_id)User is an active member of the specified orgINSERT/UPDATE/DELETE policies
is_org_admin(org_id)User has admin role in the specified orgAdmin-only operations (org settings, memberships)

RLS Policy Pattern

Standard org-scoped tables follow this pattern:

-- SELECT: any org member can read
CREATE POLICY tenant_select ON public.tenant_lookup
  FOR SELECT USING (public.can_org_read(organization_id));

-- INSERT: active members can create
CREATE POLICY tenant_insert ON public.tenant_lookup
  FOR INSERT WITH CHECK (public.can_org_write(organization_id));

-- UPDATE: active members can modify
CREATE POLICY tenant_update ON public.tenant_lookup
  FOR UPDATE USING (public.can_org_write(organization_id))
  WITH CHECK (public.can_org_write(organization_id));

-- DELETE: active members can remove
CREATE POLICY tenant_delete ON public.tenant_lookup
  FOR DELETE USING (public.can_org_write(organization_id));

Special RLS Policies

TablePolicyRationale
organizationsorg_no_delete USING (false)Organizations cannot be deleted via SQL (offboarding is a manual admin process)
audit_logINSERT only for org members; SELECT for admins; no UPDATE/DELETEAppend-only audit trail
api_keysDirect SELECT revoked; only accessible via api_keys_safe viewPrevents key hash exposure
broker_detailsRLS via JOIN to parent contacts tableChild table inherits parent org scope

4.4 Cross-Org Consistency Triggers

Database triggers enforce referential integrity across org boundaries on join tables:

TriggerTablesEnforcement
tim_comp_org_consistency()tim_comp_leasesTIM and comp must belong to the same org
tim_building_interesttim_building_interestTIM must exist in the requesting org
report_*_org_consistency()Report association tablesReport and associated entities must share org

These triggers prevent an application bug from accidentally associating records across org boundaries.

4.5 BI Export Isolation

The export schema (prism_export) provides org-filtered views for BI tools:

  • Each org gets a dedicated BI database role (e.g., prism_bi_org_123)
  • The BI role has SELECT only on export views
  • Export views are pre-filtered by organization_id
  • BI credential lifecycle is managed via one-time token exchange (see §5.5)

5. Encryption & Data Protection

5.1 Transport Encryption (TLS)

SegmentEncryptionDetails
Client → CloudflareTLS 1.2+ (terminated at edge)Managed by Cloudflare; automatic certificate provisioning
Cloudflare → BackendCloudflare Tunnel (encrypted)Internal traffic never traverses public internet
Backend → PostgreSQLTLS (pgx connection)Managed by Supabase/RDS; sslmode=require
Backend → RedisTLSManaged by Upstash; TLS enforced on connection

HSTS Policy: Strict-Transport-Security: max-age=31536000; includeSubDomains

  • 1-year HSTS max-age ensures browsers always use HTTPS after first visit
  • includeSubDomains covers all subdomains

5.2 Data at Rest Encryption

SystemEncryptionProvider
PostgreSQLAES-256 at restSupabase managed (or RDS with AWS KMS)
RedisAES-256 at restUpstash managed
Application serverEphemeral (no persistent disk)Render managed containers
BackupsEncrypted with platform encryptionSupabase / RDS automated backups

Prism does not implement application-level field encryption — encryption at rest is delegated to the managed database and cache platforms, which is appropriate for the data classification (commercial real estate intelligence, no PII beyond contact names/emails).

5.3 JWT Cryptographic Verification

PropertyValue
AlgorithmRSA (RS256, RS384, RS512)
Key managementClerk-managed JWKS endpoint with automatic key rotation
HMAC rejectionHMAC algorithms (HS256, HS384, HS512) are explicitly rejected to prevent algorithm confusion attacks
Key caching1-hour TTL with background refresh
Signature verificationEvery request — no caching of verification results

5.4 API Key Hashing

PropertyValue
AlgorithmSHA-256 (one-way hash)
Raw key exposureShown exactly once at creation time, never stored
Database storageOnly the key_hash column is stored; the api_keys_safe view excludes it
LookupHash the provided key → match against key_hash column

SHA-256 is appropriate here because API keys are high-entropy random values (not user-chosen passwords), so brute-force/dictionary attacks are not feasible.

5.5 BI Credential One-Time Token Exchange

BI credential creation uses a secure token exchange pattern to avoid transmitting database credentials in standard API responses:

Admin creates BI credentials
  │
  ├─ 1. Backend generates credentials (DB role + password)
  ├─ 2. Backend generates a 256-bit cryptographically random token
  ├─ 3. Token stored in Redis with 5-minute TTL
  ├─ 4. API returns the token (not the credentials)
  │
  └─ Client exchanges token for credentials
        ├─ 5. Client calls exchange endpoint with token
        ├─ 6. Backend validates token, checks org ID matches
        ├─ 7. Backend returns credentials
        └─ 8. Token is immediately deleted (one-time use)

Token generation: go-backend/internal/pkg/token/token.go

// 32 bytes (256 bits) of crypto/rand, URL-safe base64 encoded
func GenerateBICredentialToken() (string, error) {
    return GenerateSecureToken(32)
}

Security properties:

  • 256-bit cryptographic randomness (crypto/rand)
  • 5-minute TTL — auto-expires if not exchanged
  • One-time use — deleted immediately after successful exchange
  • Org ID validation — token can only be exchanged by the org that created it

5.6 Secret Storage

  • No secrets in source code.env files are gitignored, .env.example has only placeholders
  • Infisical is the single source of truth for all production secrets
  • No long-lived keys in frontend — only VITE_* (public) variables are in frontend builds
  • GitHub push protection blocks commits containing detected secret patterns

See Secrets Management for the full secret inventory and rotation playbooks.


6. Middleware Security Stack

The backend applies a 13-layer middleware stack to every request, in this order:

Implementation: go-backend/cmd/api/main.go

OrderMiddlewareSourcePurpose
1sentryhttp.HandleSentry SDKCaptures panics before Chi recoverer
2middleware.RecovererChiCatches panics, returns 500 instead of crashing
3middleware.Compress(5)ChiGzip compression (level 5 balanced)
4cors.Handlergo-chi/corsCORS enforcement with explicit origin allowlist
5loggingMiddlewareCustomStructured request logging (zap)
6PrometheusMiddlewareCustomHTTP request metrics (duration, status, method)
7AuditLogger.MiddlewareCustomAudit logging for sensitive mutations
8DefaultMaxBodyCustom1MB default request body size limit
9SecurityHeadersCustomHSTS, X-Frame-Options, X-Content-Type-Options, etc.
10RateLimitByIPCustomRedis sliding-window rate limiting
11RealIPCustomExtract CF-Connecting-IP (Cloudflare real IP)
12InjectRequestIDCustomGenerate unique request ID for tracing
13AuthMiddlewareCustomJWT / API Key validation + scope enforcement

Protected route groups add additional middleware:

  • RequireScope(scope) — enforces specific permission scopes per route group
  • BulkMaxBody() / UploadMaxBody() — larger body limits for bulk/upload routes

7. Input Validation & Request Limits

7.1 Request Body Size Limits

Implementation: go-backend/internal/middleware/request_limit.go

ContextLimitApplied To
Default1 MBAll routes (global middleware)
Bulk operations10 MBBulk create/update endpoints
File upload50 MBCSV import uploads
Export100 KBExport configuration payloads

Requests exceeding the limit receive 413 Request Entity Too Large.

7.2 Domain Model Validation

Every domain entity has a Validate() method that enforces business rules before database operations:

Implementation: go-backend/internal/domain/*.go

EntityValidations
BuildingRequired fields (name, property type, class), enum value checks, numeric range checks
ContactRequired fields, exactly one company association (owner OR tenant OR brokerage firm)
CompLeaseRequired fields, date range validity, numeric range checks
TIMRequired fields, status enum check
TIMBuildingInterestInterest type enum check (interest, backup, toured, proposed, offered, rejected)
KeyPointRequired fields, valid coordinates
BuildingParkRequired name
ContactActivityRequired fields, valid activity type
UserOrgMembershipRequired user ID, org ID, valid role

Validation is called in the service layer before any repository operation. Invalid input returns 400 Bad Request with structured field-level errors.

7.3 SQL Injection Prevention

All database queries use parameterized queries via pgx:

// All user input is passed as parameters ($1, $2, ...), never interpolated
rows, err := pool.Query(ctx,
    `SELECT * FROM buildings WHERE building_id = $1 AND organization_id = $2`,
    buildingID, orgID,
)

No raw string concatenation is used in SQL queries.

7.4 API Key Length Guard

API keys provided in the X-API-Key header are rejected if longer than 512 bytes, preventing abuse via oversized inputs before hashing.


8. Rate Limiting

8.1 Algorithm

Implementation: go-backend/internal/middleware/rate_limit_redis.go

PropertyValue
AlgorithmSliding window (Redis INCR + EXPIRE)
FallbackIn-memory token bucket if Redis unavailable
KeyPer client IP (or per org/API key for authenticated requests)
Redis timeout50ms — if Redis doesn't respond in 50ms, request is allowed (fail-open for availability)

8.2 Configuration

Env VarDefaultDescription
RATE_LIMIT_PER_SECONDConfigurableRequests per second per key
RATE_LIMIT_BURSTConfigurableBurst capacity
TRUST_CLOUDFLARE_HEADERSfalseWhen true, uses CF-Connecting-IP for real client IP

8.3 Real IP Extraction

Implementation: go-backend/internal/middleware/real_ip.go

When TRUST_CLOUDFLARE_HEADERS=true:

  1. Reads CF-Connecting-IP header (single IP, set by Cloudflare)
  2. Falls back to X-Real-IPX-Forwarded-ForRemoteAddr

The CF-Connecting-IP header cannot be spoofed by clients when traffic routes through Cloudflare, as Cloudflare overwrites it. This must only be trusted when Cloudflare is the edge proxy.


9. Security Headers

Implementation: go-backend/internal/middleware/security_headers.go

HeaderValuePurpose
Strict-Transport-Securitymax-age=31536000; includeSubDomainsForce HTTPS for 1 year, all subdomains
X-Content-Type-OptionsnosniffPrevent MIME type sniffing
X-Frame-OptionsDENYPrevent clickjacking — no framing allowed
X-XSS-Protection0Disable legacy XSS auditor (rely on CSP instead)
Referrer-Policyno-referrerDon't leak URLs in referrer headers
Cache-Controlno-storePrevent caching of API responses (all responses contain org-scoped data)
Permissions-PolicyRestrictiveDisables unused browser features (camera, microphone, geolocation, etc.)

10. CORS Policy

Implementation: go-backend/cmd/api/main.go

cors.Handler(cors.Options{
    AllowedOrigins:   cfg.AllowedOrigins,  // Explicit per-environment list
    AllowedMethods:   []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-API-Key"},
    ExposedHeaders:   []string{"Link"},
    AllowCredentials: true,
    MaxAge:           300,  // Preflight cache: 5 minutes
})
PropertyDetail
Origin listConfigured via ALLOWED_ORIGINS env var; no wildcards in production
CredentialsAllowCredentials: true — cookies and auth headers are allowed
Preflight cache5-minute max-age reduces preflight requests
Startup guardApplication refuses to start if ALLOWED_ORIGINS is empty

11. Audit Trail

11.1 Audit Log

Implementation: go-backend/internal/middleware/audit.go

The audit middleware captures all data mutations on sensitive resources:

Audited Resources

Route PatternResource
/api/v1/buildings*Building CRUD
/api/v1/comps*Comp lease CRUD
/api/v1/tims*TIM CRUD (including sub-resources)
/api/v1/api-keys*API key lifecycle
/api/v1/memberships*Org membership changes
/api/v1/import-jobs*Import job lifecycle
/api/v1/export*Export and BI credential operations
/api/v1/operations*Operations queue state changes
/api/v1/admin*Admin operations

Audit Log Fields

FieldDescription
actor_user_idUser who performed the action
organization_idOrg context
actionHTTP method + path
table_nameTarget resource type
record_idTarget record ID (if applicable)
changesChanged fields with old/new values
ip_addressClient IP (via CF-Connecting-IP)
user_agentClient user agent
metadataAdditional context (JSONB)

Sensitive Field Redaction

For API key operations, the audit middleware uses an allowlist — only these fields are logged from request bodies:

  • name
  • scopes
  • expires_in_days

Key hashes and raw key values are never written to the audit log.

Audit Log Integrity

  • Append-only: RLS policy allows INSERT for org members but denies UPDATE and DELETE
  • Admin-only read: Only users with audit:read scope can query the log
  • Retention: 7 years (SOC 2 compliance) — enforced by pg_cron scheduled job

12. Error Handling & Monitoring

12.1 Error Capture

Implementation: go-backend/internal/middleware/error_capture.go

BehaviorDetail
Async persistenceError details written to error_log table asynchronously (non-blocking)
5xx → SentryServer errors are reported to Sentry with full request context
Skipped codes401 and 404 are not persisted (noise reduction)
Request IDEvery error response includes request_id for correlation

12.2 Request ID Injection

Implementation: go-backend/internal/middleware/request_id_inject.go

Every JSON error response body (4xx and 5xx) is automatically augmented with a request_id field. This ID is propagated to:

  • Structured log entries (zap)
  • Error log table entries
  • Sentry error reports
  • Frontend error display (user can report the ID for support)

12.3 Error Response Format

All API errors follow a standardized format:

{
  "error": "Forbidden",
  "message": "insufficient scope: buildings:write required",
  "request_id": "req_abc123def456"
}

Validation errors include field-level detail:

{
  "error": "Bad Request",
  "message": "validation failed",
  "field_errors": {
    "name": "required",
    "property_type": "must be one of: industrial, flex, office"
  },
  "request_id": "req_abc123def456"
}

Internal details (stack traces, SQL errors) are never exposed to clients — they are captured in Sentry and the error log.

12.4 Monitoring Stack

SystemPurposeCoverage
SentryError tracking, performance monitoringAll 5xx errors, panics, slow transactions
PrometheusMetrics collectionHTTP request duration/status, DB pool stats, cache hit rates
GrafanaMetrics visualizationDashboards for API latency, error rates, resource usage
AlertmanagerAlert routingConfigurable alerts for error spikes, performance degradation

12.5 Debug Endpoint Protection

/debug/pprof endpoints (goroutine stacks, heap dumps) are only enabled in development mode (ENV=development). They are disabled in staging and production to prevent information leakage.


13. Data Retention & Deletion

13.1 Automated Retention Policies

Implementation: database/migrations/000018_add_data_retention_policies.up.sql

Data retention is enforced via pg_cron scheduled jobs that run daily at staggered times:

TableRetention PeriodScheduleRationale
error_log90 daysDaily at 03:00 UTCOperational data — 90 days sufficient for debugging
import_jobs90 daysDaily at 03:10 UTCImport history — records imported data is in permanent tables
contact_activities2 yearsDaily at 03:20 UTCBusiness activity data — 2-year lookback window
audit_log7 yearsDaily at 03:30 UTCSOC 2 compliance requirement

Purge SQL

-- Runs daily, staggered to avoid concurrent load
DELETE FROM public.error_log WHERE occurred_at < now() - INTERVAL '90 days';
DELETE FROM public.import_jobs WHERE created_at < now() - INTERVAL '90 days';
DELETE FROM public.contact_activities WHERE created_at < now() - INTERVAL '2 years';
DELETE FROM public.audit_log WHERE occurred_at < now() - INTERVAL '7 years';

13.2 Organization Offboarding (Full Purge)

When an organization cancels their account, all org-scoped data is deleted in FK-safe order:

  1. Junction/child tables first: contact_activities, contact_owner_roles, broker assignments, building metrics/addresses/electrical, comp_leases, import_jobs
  2. Core entity tables: contacts, buildings, owner_lookup, tenant_lookup, brokerage_firm_lookup
  3. Auth/config tables: api_keys, user_org_memberships
  4. Audit log: Anonymized (not deleted) — actor_user_id set to 'deleted-org', details redacted
  5. Error log: Deleted
  6. Organization record: Deleted last

Post-deletion checklist:

  • Confirm zero rows for the org across all tables
  • Revoke any BI database roles created for the org
  • Remove org from Clerk
  • Log the deletion with requester, date, and ticket number

13.3 Individual User Removal

When a single user leaves an org that remains active:

  1. Delete user_org_memberships row for the user + org
  2. Deactivate/remove user in Clerk

Business data stays with the org. Contacts, buildings, comps, activities, and import history belong to the organization, not individual users. The created_by column is retained as an internal audit trail.

13.4 Redis Cache Expiration

CacheTTLPurpose
JWKS keys1 hourClerk signing key cache
Rate limit windowsSliding window per configRequest counting
BI credential tokens5 minutesOne-time credential exchange tokens
General cache entriesVaries by resourceQuery result caching

Redis is a cache, not durable storage. Total Redis loss causes performance degradation but no data loss.


14. Secrets Management

14.1 Architecture

PrincipleImplementation
Single source of truthInfisical manages all production secrets
Auto-syncInfisical syncs to Render (backend) and Cloudflare Pages (frontend)
No manual editsPlatform dashboards are never edited directly for secret values
No secrets in code.env files gitignored; .env.example has placeholders only
Push protectionGitHub secret scanning blocks commits containing detected patterns

14.2 Secret Inventory

SecretSensitiveRotation Cadence
DATABASE_URLYes (embedded password)90 days or on compromise
REDIS_URLYes (embedded password)90 days or on compromise
CLERK_SECRET_KEYYesOn staff change or compromise
SENTRY_AUTH_TOKENYesOn compromise
CLERK_ISSUERNoOn Clerk tenant change
CLERK_JWKS_URLNoOn Clerk tenant change
All VITE_* varsNo (public by nature)

14.3 Rotation Process

  1. Rotate the credential in the upstream service
  2. Update the value in Infisical (prod environment)
  3. Infisical auto-syncs to deployment platform
  4. Redeploy the affected service
  5. Verify health (/ready for backend, auth flow for frontend)
  6. Infisical version history serves as the rotation log

See Secrets Management for detailed per-secret rotation playbooks.

14.4 Break-Glass Access

Emergency direct database access:

  • Must be documented, audited, and the credentials rotated immediately after use
  • Uses postgres superuser role — break-glass only
  • Normal application access uses prism_app role (no SUPERUSER, no BYPASSRLS)

15. Infrastructure Security

15.1 Network Architecture

Internet → Cloudflare (WAF + TLS) → Cloudflare Tunnel → Render (Backend :8080)
                                                       → Supabase (Postgres)
                                                       → Upstash (Redis)
  • Backend port :8080 is not publicly accessible — only reachable via Cloudflare tunnel
  • Database is managed by Supabase with network-level access controls
  • Redis is managed by Upstash with TLS-only connections

15.2 Database Role Model

RolePrivilegesUsed By
postgres (superuser)Full accessBreak-glass emergency only
prism_appUSAGE on public, CRUD via RLSApplication process
authenticatedRead public, org-scoped CRUD via RLSSupabase auth context
service_roleExecute refresh/snapshot functionsScheduled jobs, export refresh

Key constraint: prism_app has no SUPERUSER and no BYPASSRLS — all application access goes through RLS policies.

15.3 Platform Access Control

SystemAdmin AccessStandard Access
GitHubEngineering lead (Owner/Admin)Active engineers (Write)
InfisicalBackend lead / Ops leadEngineers (read-only prod)
Supabase/RDSBackend lead (break-glass only)API process only (prism_app role)
RedisBackend lead
CloudflareOps lead
SentryBackend leadActive engineers (Member)
RenderOps lead
Clerk DashboardBackend lead

15.4 CI/CD Security

ControlImplementation
Branch protectionmain branch requires PR review
Automated testingGitHub Actions runs on every PR (tests, lint, typecheck, vet)
PostGIS test containerIntegration tests run against real Postgres with full schema
Race detectiongo test -race in CI catches data races
Swagger drift checkmake api-docs-check fails CI if generated docs are stale
Secret scanningGitHub push protection blocks secret patterns pre-commit
Dependency scanningDependabot monitors Go modules and npm packages
Static analysisCodeQL runs in CI pipeline

16. Vulnerability Management

16.1 Dependency Scanning

ToolScopeFrequency
DependabotGo modules, npm packagesContinuous (PRs on new CVEs)
CodeQLGo, TypeScript static analysisPer PR
GitHub secret scanningAll file typesContinuous

16.2 Response SLAs

SeverityResponse TimeActions
Critical (CVSS 9.0+)24 hoursImmediate patch, emergency deploy
High (CVSS 7.0-8.9)72 hoursPrioritized patch
Medium (CVSS 4.0-6.9)2 weeksScheduled update
Low (CVSS < 4.0)Next maintenance cycleBatched with other updates

See Vulnerability Management for full procedures.


17. Incident Response

17.1 Severity Levels

SeverityResponse TargetExample
P1 — Critical< 15 minutesProduction down, confirmed data breach, auth bypass
P2 — High< 1 hourElevated 5xx rate, auth failures, critical CVE
P3 — Medium< 4 hoursSingle endpoint issue, staging problem
P4 — LowNext business dayNon-critical dep alert, doc drift

17.2 Detection Sources

SourceDetects
Sentry alertsError spikes, performance degradation
Render metricsCPU, memory, container restarts
GitHub DependabotNew CVEs in dependencies
GitHub secret scanningLeaked credentials
Audit log anomaliesUnusual mutation patterns
External reportsUser-reported issues

17.3 Response Process

  1. Detect — alert triggers or report received
  2. Triage — assign severity, notify on-call
  3. Contain — isolate affected systems (revoke keys, block IPs, disable features)
  4. Investigate — correlate request IDs, audit logs, error logs, Sentry events
  5. Remediate — deploy fix, rotate compromised credentials
  6. Recover — verify system health, confirm data integrity
  7. Postmortem — document timeline, root cause, preventive actions

See Incident Response for full procedures.


18. Backup & Disaster Recovery

18.1 Objectives

ObjectiveTarget
RPO (Recovery Point Objective)24 hours (daily backup minimum)
RTO (Recovery Time Objective)4 hours
Uptime target99.5% monthly

18.2 Backup Strategy

DataBackup MethodRetention
PostgreSQLSupabase daily automated (PITR on Pro)7-30 days
RedisNot backed up (cache only)N/A
Import staging filesNot backed up (transient)N/A

Pre-migration: Manual snapshot required before every production database migration.

18.3 Degraded Mode Behavior

Dependency DownImpact
RedisCache misses fall through to DB; all endpoints work with degraded performance
DatabaseAPI returns 503; /health returns 200, /ready returns 503
Clerk JWKSTolerated during cache TTL (1 hour); extended outage blocks all user auth
CloudflareAPI unreachable from internet; process continues running internally

18.4 Restore Drills

Restore drills must be performed quarterly. Drill records are stored in docs/audit/restore-drills/.

See Backup & Disaster Recovery for full restore procedures.



Source Code References

TopicFile
Auth middleware (JWT + API Key)go-backend/internal/middleware/auth.go
Clerk JWT validatorgo-backend/internal/auth/clerk.go
Auth context utilitiesgo-backend/internal/auth/context.go
Scope definitionsgo-backend/internal/domain/auth.go
Security headersgo-backend/internal/middleware/security_headers.go
Rate limiting (Redis)go-backend/internal/middleware/rate_limit_redis.go
Audit logginggo-backend/internal/middleware/audit.go
Error capturego-backend/internal/middleware/error_capture.go
Request ID injectiongo-backend/internal/middleware/request_id_inject.go
Real IP extractiongo-backend/internal/middleware/real_ip.go
Body size limitsgo-backend/internal/middleware/request_limit.go
BI credential tokengo-backend/internal/pkg/token/token.go
Middleware stack (main)go-backend/cmd/api/main.go
RLS policiesdatabase/PRISM_vNext_FINAL.sql (section 14)
Data retention jobsdatabase/migrations/000018_add_data_retention_policies.up.sql