This whitepaper is for engineering teams doing technical due diligence, security and compliance officers, and power users auditing their third-party vendors. Every claim below maps to code or infra config — no marketing language.
Encryption in transit and at rest
- All traffic is TLS 1.2+ (ALB / Cloudflare layer forces HTTPS; HSTS set by the edge proxy)
- OAuth refresh / access tokens are AES-256-GCM encrypted with a unique 96-bit IV per row (`lib/crypto.ts`). The `tokenVersion` column supports staged re-encryption when we rotate keys
- API keys are stored as SHA-256 hashes (plaintext prefix only for UI identification). The full plaintext is shown exactly once at creation — a DB dump cannot reconstruct any key
- Passwords are bcrypt-hashed (cost factor 12)
- Password-reset and email-verification tokens are also SHA-256 hashed + 24h auto-expiry + single-use
Audit trail
Every account-level security event is recorded in the `audit_events` table — sign-in / sign-out (with IP + User-Agent), password change, API key issuance / revocation, OAuth connect / disconnect / token expiry, subscription change / cancellation.
- Users see their last 10 events live in /settings#security
- Operators see the org-wide last 12 in /admin (gated by ADMIN_EMAILS allowlist)
- SDKs can pull the full per-user history via `/api/v1/audit-events` (supports `?since` + `?kind=` filtering)
- A weekly cron prunes events older than 180 days (configurable via `AUDIT_RETENTION_DAYS`)
- A composite `(userId, createdAt DESC)` index keeps per-user lookups index-only
Self-serve user rights (GDPR / CCPA)
Self-serve paths come first — users don't have to wait for support to exercise their data rights:
- Export — `GET /api/account/export` returns a full JSON snapshot (profile + subscriptions + ad_accounts + api_keys metadata + last 90 days of audit_events). Secrets never appear
- Delete — one-click account closure in Settings Danger Zone, cascades through every FK
- Correct — edit display name, locale, AI provider, email preferences directly in /settings
- Revoke OAuth — one-click disconnect in Settings, or directly in the provider console
Stripe idempotency
Stripe's at-least-once delivery means the same event will be retried. We dedupe via the `stripe_events` table: INSERT the event ID, PK conflict = duplicate = ack 200 immediately. Failure DELETEs the row so Stripe's retry can attempt again. This prevents double-writes on subscription state and double-counts on revenue.
Content security and defense in depth
- Content-Security-Policy runs in Report-Only mode (`default-src self / frame-ancestors none / upgrade-insecure-requests`); violations POST to `/api/csp-report` and land in CloudWatch
- X-Frame-Options: DENY (clickjacking guard, duplicates frame-ancestors for older browsers)
- X-Content-Type-Options: nosniff (defeats MIME confusion attacks)
- Referrer-Policy: strict-origin-when-cross-origin
- Permissions-Policy disables camera / microphone / geolocation / interest-cohort / browsing-topics
- Honeypot field on the contact form silently drops bot submissions
Observability and incident response
- Structured JSON logs (`lib/logger`) ship to CloudWatch
- `captureError` is the central error hook; forwards to Sentry when `SENTRY_DSN` + `globalThis.__sentry` are set (optional, no hard dep)
- `/api/health` endpoint backs the ALB / ECS healthcheck (process up + DB reachable)
- Public /status page shows live service state + 90 days of incident history
AI calls and data retention
We use Anthropic Claude and OpenAI's enterprise APIs (explicitly not used for training). Users can switch providers or fall back to the system default in /settings. The AI receives only a snapshot of the current account's mock ad data — never raw OAuth tokens, password hashes, or another user's data.
Roadmap and compliance
- SOC 2 Type II audit scheduled for 2026 Q4
- Flip CSP from Report-Only to enforcing mode once reports stay quiet
- Optional SCIM / SAML SSO (enterprise plan)
- Key-event email notifications (new-device sign-in, API key creation)