Skip to main content

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 the x-webhook-secret header (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"
}