Uptime & Data Residency
- Availability target
- 99.5% monthly uptime
- Recovery time objective
- 4 hours (recovery time objective)
- Recovery point objective
- 24 hours (recovery point objective, Cloud SQL automated backups)
- Data residency
- australia-southeast1 (Sydney, Australia) — all tenant data at rest
Security Controls
15 controls across 7 categories. Updated 2026-06.
Encryption Data Isolation Access Control Audit Data Minimisation Secrets Management Observability
| Category | Control | Evidence |
|---|---|---|
| Encryption | Encryption in transit All API and web traffic is served over HTTPS/TLS 1.2+. HTTP is not supported in production. | GCP Cloud Run managed TLS; trust page served on HTTPS origin |
| Encryption | Encryption at rest All tenant databases are encrypted at rest using AES-256 at the storage layer. | Cloud SQL AES-256 encryption at rest (platform-managed) |
| Data Isolation | Database per tenant Every customer tenant has its own dedicated Postgres database. No shared connection pool or shared schema. A bug in one tenant's DB query cannot touch another tenant's data. | db-per-tenant architecture; createDb() bound to TENANT_DATABASE_URL per deployment |
| Data Isolation | Row-level security on every table Every issuer-scoped table has FORCE RLS enabled. An unauthenticated or cross-issuer query returns zero rows — no data is accidentally exposed. | Migration-first FORCE RLS on every table; db.test.ts verifies cross-issuer isolation |
| Access Control | API key authentication All issuer API access requires a SHA-256-hashed API key. Keys are never stored in plain text and are only shown once at creation. | api_keys table; SHA-256 hashed on creation; issueApiKey() domain function |
| Access Control | Rate limiting with 429 + Retry-After All public endpoints are rate-limited per API key (or IP) with a standard 429 response that includes a Retry-After header. Prevents abuse and protects tenants from noisy-neighbour effects. | @fastify/rate-limit; S-PUBSEC-01 standard; api.test.ts asserts 429 + Retry-After |
| Audit | Append-only audit log All credential lifecycle events (issue / revoke / reissue / GDPR erase) are written to an append-only audit_log table. UPDATE, DELETE, and TRUNCATE are blocked at the database level by a trigger — the log cannot be silently altered. | audit_log, verification_events, engagement_events — S-TRUNCATE-01; db.test.ts asserts TRUNCATE rejection |
| Audit | Credential lifecycle immutability Revocation and expiry are reflected in real time without erasing the credential record. The effective status is computed at serve time from the stored row, so the history is always intact. | effectiveStatus() in domain/status.ts; revokedAt / expiresOn columns; verify page reflects status instantly |
| Data Minimisation | No raw recipient email storage Recipient email is NEVER stored or logged raw. Only a per-issuer salted SHA-256 hash is stored. A GDPR erase nulls the display name and revokes credentials; the salted hash is a retained pseudonymous tombstone. | recipientHash in credentials table; domain/ingest.ts; domain/gdpr.ts; hardening.test.ts asserts raw email never returned |
| Data Minimisation | GDPR subject access + erasure POST /api/v1/gdpr/export returns a recipient's pseudonymous data (hash + label + credentials, never the raw email). POST /api/v1/gdpr/erase nulls the personal label, revokes all their credentials, and logs the event. | domain/gdpr.ts; hardening.test.ts |
| Secrets Management | No secrets in source control Signing keys, API tokens, and database connection strings are never committed. A CI gate (npm run check:secrets) scans every commit and blocks merges if any secret pattern is detected. | scripts/check-secrets.sh; CI step check:secrets; STUBS.md lists all operator-gated secrets |
| Secrets Management | GCP Secret Manager for production secrets All production secrets (TENANT_DATABASE_URL, SendGrid key, signing keys) are stored in GCP Secret Manager and injected at runtime. None appear in environment variables, config files, or container images. | cloudbuild.yaml --update-secrets; infra/terraform/secrets.tf secret shells; docs/DEPLOYMENT.md env/secret matrix |
| Observability | Structured request logging Production API logs are structured pino/JSON with a per-request correlation ID (x-request-id). Raw email addresses and secrets never appear in logs. | server.ts logger config; domain/notify.ts log scrub |
| Observability | Prometheus metrics + OpenTelemetry bootstrap GET /metrics serves Prometheus-format metrics (issued total, verified total, HTTP request count + latency histogram). OpenTelemetry traces are exported when OTEL_EXPORTER_OTLP_ENDPOINT is configured. | domain/metrics.ts; GET /metrics; CRD-12c |
| Observability | Health + readiness probes GET /health confirms liveness. GET /health/ready confirms the DB is reachable before accepting traffic. Used by Cloud Run startup/liveness probes. | server.ts; /health and /health/ready endpoints; cloud-run.service.yaml probe config |
Subprocessors
Third-party services that process customer data on our behalf.
| Processor | Purpose | Location | Data types |
|---|---|---|---|
| Google Cloud Platform | Application hosting (Cloud Run), tenant database hosting (Cloud SQL), secrets storage (Secret Manager), container registry (Artifact Registry) | australia-southeast1 (Sydney, Australia) | • Tenant databases • Application secrets • Container images • Application logs |
| SendGrid (Twilio) | Transactional email delivery — credential issuance notifications, earner portal login codes, renewal alerts | United States | • Issuer email address (from-address config) • Recipient email address (transient in memory — never stored; used only to address the outbound email) |
| GitHub / GitHub Actions | Source code hosting, CI/CD pipeline (build, test, deploy) | United States | • Application source code • Build artefacts • Deployment configuration (no secrets — these are in Secret Manager) |
Contact
Security questions: security@cohiva.com
Trust documentation last reviewed: 2026-06
Machine-readable controls: GET /api/v1/trust/controls