Security
Security
This document covers the security architecture of the Groundtruth Platform, including tenant isolation, access control, data protection, and compliance measures.
Row-Level Security (RLS)
All database tables have Row-Level Security policies enforced at the PostgreSQL level via Supabase. RLS provides a critical defense-in-depth layer:
- Every query is scoped by
tenantIdat the database level - Even if application code has a bug (missing WHERE clause, incorrect join), cross-tenant data access is blocked by PostgreSQL itself
- Policies are applied and versioned through Prisma migrations
- The Supabase service role key bypasses RLS for administrative operations (used only server-side, never exposed to the client)
RLS ensures that Tenant A can never read, update, or delete Tenant B's data regardless of application-layer defects.
Role-Based Access Control (RBAC)
Role Hierarchy
Four roles are defined in a strict hierarchy where each role inherits the permissions of all roles below it:
| Role | Permissions |
|---|---|
| owner | Full access + billing + team management + delete tenant |
| admin | Create/manage engagements + team members + view costs + admin dashboard |
| member | Create/run engagements + view deliverables |
| viewer | Read-only access (intended for client stakeholders with accounts) |
Enforcement
- Role checks are performed in middleware on a per-route basis via
rbac.ts - The role hierarchy is enforced: a user cannot assign a role equal to or higher than their own
- Registration creates the first user with the owner role
- Subsequent users are invited with a specified role (default: member)
Route Protection Examples
| Route Pattern | Minimum Role |
|---|---|
/dashboard/admin/* | admin |
/api/team/* | admin |
/api/engagements/*/run | member |
/dashboard/engagements/* (read) | viewer |
/api/billing/* | owner |
See monitoring.md for details on the admin health dashboard access requirements.
Security Headers
The following HTTP security headers are configured in next.config.ts and applied to all responses:
| Header | Value | Purpose |
|---|---|---|
X-Frame-Options | DENY | Prevents clickjacking by blocking iframe embedding |
X-Content-Type-Options | nosniff | Prevents MIME type sniffing attacks |
Referrer-Policy | strict-origin-when-cross-origin | Limits referrer information leakage |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Enforces HTTPS for 1 year including subdomains |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Disables unnecessary browser APIs |
Content-Security-Policy | Restrictive policy | Allows self, Stripe, Supabase, and Sentry domains only |
The Content-Security-Policy is configured to allow only the specific external domains required by the platform (Stripe.js for payment forms, Supabase for auth/storage, Sentry for error tracking) while blocking all other inline scripts and external resources.
Rate Limiting
Implementation
Rate limiting uses a two-layer approach:
- Primary — Redis-backed sliding window via
@upstash/ratelimit(Upstash Redis) - Fallback — In-memory token bucket if Redis is unavailable (graceful degradation)
Tiers
Three rate limiting tiers are configured based on endpoint sensitivity:
| Tier | Use Case | Window | Limit |
|---|---|---|---|
| api | General API endpoints | Sliding window | Standard requests per minute |
| engine | Engine execution endpoints | Sliding window | Strict limit (expensive operations) |
| billing | Billing and payment endpoints | Sliding window | Strictest limit (financial operations) |
Identification
Rate limits are applied per API key (for API requests) or per tenant (for session-based requests).
Response
When a rate limit is exceeded, the server returns:
HTTP 429 Too Many Requests
Retry-After: <seconds>The Retry-After header indicates how many seconds the client should wait before retrying.
API Key Security
Storage
- API keys are hashed with SHA-256 before being stored in the database
- The raw key is displayed exactly once at creation time and is never retrievable afterward
- Keys are prefixed with
gt_for easy identification in logs and configuration
Lifecycle
| Operation | Description |
|---|---|
| Creation | Generates a cryptographically random key, stores SHA-256 hash, returns raw key once |
| Authentication | Incoming key is hashed and compared against stored hashes |
| Revocation | Soft revocation via revokedAt timestamp (key record preserved for audit trail) |
| Rotation | Create a new key, revoke the old one. Both can be active during a transition period |
Scoping
Each API key has a defined scope:
| Scope | Permissions |
|---|---|
read_only | GET requests only (list engagements, read deliverables) |
read_write | Full CRUD operations (create engagements, trigger runs) |
Keys can be scoped to a specific engagement or granted tenant-wide access.
Audit Logging
AuditLog Model
Every significant action in the platform is recorded in the AuditLog table with the following fields:
| Field | Type | Description |
|---|---|---|
action | String | The action performed (e.g., engagement.create, team.invite) |
userId | String | The user who performed the action |
tenantId | String | The tenant context |
resourceType | String | Type of resource affected (e.g., engagement, api_key) |
resourceId | String | ID of the affected resource |
metadata | JSON | Additional context (varies by action) |
ipAddress | String | Client IP address |
createdAt | DateTime | Timestamp of the action |
Tracked Actions
The following categories of actions are logged:
- Authentication — Login, logout, password reset, magic link usage
- Engagement CRUD — Create, update, delete engagements
- Run controls — Start, stop, pause, resume engagement runs
- API key management — Create, revoke, rotate keys
- Team changes — Invite user, change role, remove user
- Billing — Subscription changes, payment events
- Data export/deletion — GDPR data export requests, account deletion requests
- Webhook management — Register, update, delete webhook endpoints
GDPR Compliance
Data Export
GET /api/account/exportReturns a ZIP archive containing all tenant data in JSON and markdown formats:
- Tenant profile and settings
- All users and their roles
- All engagements with configuration
- All deliverables (content + metadata)
- All runs with cost data
- Attachments
- Audit logs
Account Deletion
DELETE /api/account/deletePerforms a complete account deletion:
- Cancels Stripe subscription (if active)
- Deletes all tenant data (engagements, deliverables, runs, attachments, users)
- Anonymizes audit log entries (replaces user identifiers with "deleted-user")
- Removes the tenant record
- Sends confirmation email
A DataDeletionRequest record is created to track the deletion process and provide evidence of compliance.
Cookie Consent
A cookie consent banner is displayed to new visitors. The banner:
- Explains cookie usage (essential + analytics)
- Requires explicit consent before setting non-essential cookies
- Stores consent preference in a cookie (the only cookie set before consent)
Data Retention
- Engagements older than 2 years are automatically archived (
archivedAttimestamp set on the Engagement model) - Archived engagements are excluded from default queries but remain accessible if explicitly requested
- Full deletion requires an explicit account deletion request
Webhook Security
Outbound webhook payloads are signed using HMAC-SHA256:
- Each webhook endpoint registration generates a unique signing secret
- Every outbound payload includes an
X-Groundtruth-Signatureheader - The signature is computed as
HMAC-SHA256(signing_secret, raw_request_body) - Recipients verify the signature to confirm the payload originated from Groundtruth and was not tampered with
The signing secret is displayed once at registration and can be rotated.
Client Portal Security
The client portal provides read-only access to deliverables without requiring a Groundtruth account:
| Mechanism | Description |
|---|---|
| Token-based access | Portal links include a unique access token |
| Token storage | Tokens are hashed with SHA-256 before storage (same pattern as API keys) |
| Expiration | Tokens can have an optional expiration date |
| Revocation | Tokens can be revoked by the engagement owner at any time |
| Scope | Each token is scoped to a single engagement |
Portal users can view deliverables, leave comments, and approve/reject deliverables. They cannot modify engagement configuration, access other engagements, or view billing information.
Related Documentation
- Monitoring — Error tracking, logging, health checks, admin dashboard
- Environment Variables — All configuration variables including secrets and API keys