Authentication System
Kinetic Email uses Supabase Auth with a multi-layer authentication and authorization model.
Authentication Methods
| Method | Implementation |
|---|---|
| Magic Link (OTP) | Email-based passwordless login |
| Google OAuth | Social login via Google |
| Email/Password | Traditional credentials |
All methods are configured through AuthContext.tsx and powered by the Supabase Auth SDK.
Auth Flow
User clicks "Sign In"
│
▼
LoginModal → AuthContext.signIn()
│
▼
Supabase Auth (magic link / OAuth / password)
│
▼
JWT issued → stored in browser session
│
├── Frontend: AuthContext provides user state
│ └── Components use useAuth() hook
│
└── Backend: JWT passed as Bearer token
└── API functions call verifyUser(req, res)
Frontend Auth (AuthContext.tsx)
The AuthContext provides authentication state to the entire app:
const { user, isAdmin, signIn, signUp, signOut } = useAuth();
Key features:
- Admin detection — Checks
user_metadata.is_adminflag, verified againstadmin_userstable - Profile management — User profiles stored in
user_profilestable (display name, preferences) - Referral tracking — Captures
?ref=URL parameters and processes viaprocess_referral()RPC - Guest → User sync — Learning progress from localStorage is synced to database on signup
Backend Auth (api/_auth.ts)
Two authentication levels for API endpoints:
verifyUser(req, res)
Validates the JWT from the Authorization: Bearer <token> header. Any logged-in user passes.
// Usage in any API endpoint
const user = await verifyUser(req, res);
if (!user) return; // 401 already sent
// user.id, user.email available
Returns:
userobject on successnullon failure (sends 401 response automatically)
verifyAdmin(req, res)
Same as verifyUser plus checks the admin_users table for the user's email.
const user = await verifyAdmin(req, res);
if (!user) return; // 401 or 403 already sent
Returns:
userobject on successnullon failure (sends 401 for invalid token, 403 for non-admin)
Token Spending (spendTokensServer)
Server-side token deduction for premium actions:
const tokenResult = await spendTokensServer(req, res, 'ai_generation');
if (!tokenResult) return; // 402 already sent
This creates a Supabase client authenticated as the user (using their JWT + anon key) so that auth.uid() resolves correctly inside the spend_tokens RPC function. The service role key is not used — this ensures RLS policies are respected.
Auth Modules
| File | Language | Used By |
|---|---|---|
api/_auth.ts | TypeScript | send-email.ts, send-email-ses.ts, generate-email.ts |
api/admin/_auth.js | JavaScript | generate.js, all api/admin/*.js routes |
Both modules export identical functions: verifyUser, verifyAdmin, spendTokensServer, setCorsHeaders.
Security Model
┌─────────────────────────────────────────────────┐
│ Layer 1: Supabase Auth (JWT validation) │
│ → verifyUser() — any authenticated user │
├─────────────────────────────────────────────────┤
│ Layer 2: Admin Check (admin_users table) │
│ → verifyAdmin() — email in admin_users table │
├─────────────────────────────────────────────────┤
│ Layer 3: Token Economy (spend_tokens RPC) │
│ → spendTokensServer() — sufficient balance │
├─────────────────────────────────────────────────┤
│ Layer 4: Supabase RLS (row-level security) │
│ → Database enforces auth.uid() = user_id │
└─────────────────────────────────────────────────┘