API Authentication
All authenticated API endpoints expect a Supabase JWT in the Authorization header.
Obtaining a Token
Tokens are obtained through Supabase Auth on the frontend. The supabase.ts library provides helper functions:
import { supabase, getAuthHeaders } from '../lib/supabase';
// Get the current session
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token;
// Or use the helper
const headers = await getAuthHeaders();
// Returns: { Authorization: 'Bearer <token>', 'Content-Type': 'application/json' }
Making Authenticated Requests
const response = await fetch('/api/generate', {
method: 'POST',
headers: await getAuthHeaders(),
body: JSON.stringify({ prompt: 'Create a tabbed email...' }),
});
Auth Levels
User Auth (verifyUser)
Any logged-in user with a valid JWT passes. Used for:
- Email generation (all three pipeline stages: copy, blueprint, code)
- Email sending via Mailgun (the sender address is always the verified session user)
- Unsubscribe (logged-in path)
Admin Auth (verifyAdmin)
Valid JWT + email must be in the admin_users table. Used for:
- RAG content management
- QA evaluations
- System statistics
Token Gating (spendTokensServer)
After user auth passes, premium actions (e.g. ai_generation on /api/generate and /api/generate-email) also require sufficient token balance. The server calls the spend_tokens RPC function atomically and responds 402 when the balance is insufficient.
Non-JWT Auth
A few endpoints authenticate without a Supabase JWT:
/api/mailgun-webhook— Mailgun HMAC-SHA256 signature (MAILGUN_WEBHOOK_SIGNING_KEY, mandatory; requests fail closed if unset)/api/notify-signup— shared secret in thex-webhook-secretheader (SIGNUP_WEBHOOK_SECRET)/api/unsubscribe(anonymous path) — signed HMAC link token (UNSUB_TOKEN_SECRET); bare emails are rejected
Error Responses
401 — Invalid or Missing Token
{
"error": "Authentication required"
}
403 — Not an Admin
{
"error": "Forbidden: admin access required"
}
402 — Insufficient Tokens
{
"error": "Insufficient tokens",
"message": "Not enough tokens for this action"
}