Skip to main content

Security Overview

Kinetic Email implements defense-in-depth security across all layers of the stack.

Security Layers

┌─────────────────────────────────────────────┐
│ 1. CORS Restriction │
│ Only kinetic.email + localhost origins │
├─────────────────────────────────────────────┤
│ 2. JWT Authentication │
│ Supabase JWT verified on every request │
├─────────────────────────────────────────────┤
│ 3. Admin Authorization │
│ admin_users table whitelist │
├─────────────────────────────────────────────┤
│ 4. Token Economy │
│ Server-side atomic token spending │
├─────────────────────────────────────────────┤
│ 5. Row-Level Security (RLS) │
│ Database enforces user isolation │
├─────────────────────────────────────────────┤
│ 6. Input Sanitization │
│ XSS prevention on user inputs │
├─────────────────────────────────────────────┤
│ 7. Security Headers │
│ X-Content-Type-Options, X-Frame-Options │
└─────────────────────────────────────────────┘

Key Protections

CORS Enforcement

API endpoints only accept requests from:

  • https://kinetic.email
  • https://www.kinetic.email
  • http://localhost:5173 (development)
  • http://localhost:3000 (development)

Configured in vercel.json headers and setCorsHeaders() function.

Authentication

All API endpoints (except public tracking pixels, "View in Browser", public RAG stats, and blog OG images) require a valid Supabase JWT in the Authorization: Bearer <token> header. Supabase Auth uses the PKCE flow.

Two auth levels:

  • verifyUser() — Any authenticated user
  • verifyAdmin() — Authenticated user + admin_users table match

Email sending is auth-required and always attributed to the verified account email, not a client-supplied value.

Webhook & Token-Based Protections

  • Mailgun webhooks — HMAC-SHA256 signature verification is required (MAILGUN_WEBHOOK_SIGNING_KEY); unsigned events are rejected
  • Unsubscribe — requires either a verified session or a signed HMAC token (UNSUB_TOKEN_SECRET); anonymous unsubscribes without a valid token are rejected
  • Signup alerts — Supabase webhook to api/notify-signup.ts, authenticated with a shared secret (SIGNUP_WEBHOOK_SECRET)

Tracking Privacy

Tracking events store hashed recipient emails, and IP addresses are salted-and-hashed with IP_SALT — if the salt is missing or too short, the IP is dropped from the event entirely.

Server-Side Token Spending

Token deduction happens atomically on the server via the spend_tokens RPC function. The frontend only performs read-only balance checks for UX feedback. This prevents users from bypassing token costs by calling APIs directly.

XSS Prevention

User-supplied content (like email addresses) is HTML-escaped before insertion into email HTML:

function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

Security Headers

Set via vercel.json:

  • X-Content-Type-Options: nosniff — Prevents MIME type sniffing
  • X-Frame-Options: DENY — Prevents clickjacking
  • Cache-Control — Appropriate caching for static vs dynamic content

Secret Management

  • Environment variables stored in Vercel project settings — see Environment Variables for the full list
  • SUPABASE_SERVICE_ROLE_KEY never exposed to frontend; used only by serverless functions for service-role-only tables (rag_documents, eval tables, suppressions, delivery events)
  • VITE_ prefixed vars are safe to expose (anon keys only)
  • API keys rotated through Vercel env var updates

Reporting Security Issues

If you discover a security vulnerability, please report it responsibly via email rather than opening a public issue.