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.emailhttps://www.kinetic.emailhttp://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 userverifyAdmin()— Authenticated user +admin_userstable 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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
Security Headers
Set via vercel.json:
X-Content-Type-Options: nosniff— Prevents MIME type sniffingX-Frame-Options: DENY— Prevents clickjackingCache-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_KEYnever 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.