Architecture: Email System
Email System Architecture
Overview
Email is a per-project concern, not platform infrastructure. basketball-api owns all Westside email via
gmail-sdk (direct import, no microservice indirection). pal-e-mail is archived — it was never consumed.Components
- gmail-sdk — shared Python library. OAuth token management, send, refresh.
- services/email.py — send functions, template loading, EmailLog persistence
- services/email_queries.py — audience query registry (unsigned_contracts, etc.)
- services/outbox.py — async event-driven sends (contract_signed)
- brand.py — design tokens (colors, fonts, CSS)
- templates/email/*.mjml — MJML source files, compiled during docker build
- load_email_template() — reads compiled HTML, replaces {{key}} placeholders
- EmailLog — audit trail: tenant, parent, player, type, gmail_message_id
Layouts
- notification — "X happened" (headline + body + footer)
- action — "Do this thing" (headline + body + CTA button + footer)
- announcement — rich content (headline + sections + optional CTA + footer)
Email Flow
MJML source → compile during docker build → baked into image → load_email_template() at runtime → gmail-sdk send → EmailLog
Preview & Approval Workflow
Development (fast iteration)
- Run
mjml --watch templates/email/*.mjml -o templates/email/compiled/— recompiles on save - Open compiled HTML in Chrome DevTools at 390px width (mobile viewport)
- Edit MJML → save → refresh browser → see changes instantly
- This gets layout and branding 90% right. Not pixel-perfect for Gmail but close enough for structural work.
Approval (the real gate)
- Deploy image with compiled templates (or use test endpoint on dev)
- Hit
/email/blastwithtest_email=lucas@...— sends one real email - Check on phone (Gmail app). This is the truth — real renderer, real viewport.
- Forward to Marcus if he needs to approve content
- Lucas approves → blast to full audience
Why not a live preview server?
Gmail's HTML renderer is proprietary. No local tool can perfectly simulate it. Browsers support CSS that Gmail strips (Grid, Flexbox, custom properties, <style> blocks). MJML handles the hard part by compiling to table-based inline styles, but the only true test is sending a real email. Paid services (Litmus, Email on Acid) render across 90+ clients — overkill for 3 layouts targeting mostly Gmail mobile.
Decisions
- No CSS Grid/Flexbox/custom properties in email (Gmail strips them). MJML compiles to table-based inline styles.
- No Jinja2 — simple {{key}} string replacement via load_email_template()
- Mobile-first via MJML default column stacking
- test_email query param on all blast endpoints for safety
- Browser preview for dev speed, real Gmail send for approval gate