{"id":1102,"title":"Architecture: Email System","slug":"arch-email","html_content":"<h2 id=\"email-system-architecture\">Email System Architecture</h2>\n<h3 id=\"overview\">Overview</h3>\n<p>Email is a per-project concern, not platform infrastructure. basketball-api owns all Westside email via <code>gmail-sdk</code> (direct import, no microservice indirection). <code>pal-e-mail</code> is archived — it was never consumed.</p>\n<h3 id=\"components\">Components</h3>\n<ul>\n<li><strong>gmail-sdk</strong> — shared Python library. OAuth token management, send, refresh.</li>\n<li><strong>services/email.py</strong> — send functions, template loading, EmailLog persistence</li>\n<li><strong>services/email_queries.py</strong> — audience query registry (unsigned_contracts, etc.)</li>\n<li><strong>services/outbox.py</strong> — async event-driven sends (contract_signed)</li>\n<li><strong>brand.py</strong> — design tokens (colors, fonts, CSS)</li>\n<li><strong>templates/email/*.mjml</strong> — MJML source files, compiled during docker build</li>\n<li><strong>load_email_template()</strong> — reads compiled HTML, replaces {{key}} placeholders</li>\n<li><strong>EmailLog</strong> — audit trail: tenant, parent, player, type, gmail_message_id</li>\n</ul>\n<h3 id=\"layouts\">Layouts</h3>\n<ul>\n<li><strong>notification</strong> — \"X happened\" (headline + body + footer)</li>\n<li><strong>action</strong> — \"Do this thing\" (headline + body + CTA button + footer)</li>\n<li><strong>announcement</strong> — rich content (headline + sections + optional CTA + footer)</li>\n</ul>\n<h3 id=\"email-flow\">Email Flow</h3>\n<p>MJML source → compile during docker build → baked into image → load_email_template() at runtime → gmail-sdk send → EmailLog</p>\n<h3 id=\"preview-approval-workflow\">Preview &amp; Approval Workflow</h3>\n<h4 id=\"development-fast-iteration\">Development (fast iteration)</h4>\n<ol>\n<li>Run <code>mjml --watch templates/email/*.mjml -o templates/email/compiled/</code> — recompiles on save</li>\n<li>Open compiled HTML in Chrome DevTools at 390px width (mobile viewport)</li>\n<li>Edit MJML → save → refresh browser → see changes instantly</li>\n<li>This gets layout and branding 90% right. Not pixel-perfect for Gmail but close enough for structural work.</li>\n</ol>\n<h4 id=\"approval-the-real-gate\">Approval (the real gate)</h4>\n<ol>\n<li>Deploy image with compiled templates (or use test endpoint on dev)</li>\n<li>Hit <code>/email/blast</code> with <code>test_email=lucas@...</code> — sends one real email</li>\n<li>Check on phone (Gmail app). This is the truth — real renderer, real viewport.</li>\n<li>Forward to Marcus if he needs to approve content</li>\n<li>Lucas approves → blast to full audience</li>\n</ol>\n<h4 id=\"why-not-a-live-preview-server\">Why not a live preview server?</h4>\n<p>Gmail's HTML renderer is proprietary. No local tool can perfectly simulate it. Browsers support CSS that Gmail strips (Grid, Flexbox, custom properties, &lt;style&gt; 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.</p>\n<h3 id=\"decisions\">Decisions</h3>\n<ul>\n<li>No CSS Grid/Flexbox/custom properties in email (Gmail strips them). MJML compiles to table-based inline styles.</li>\n<li>No Jinja2 — simple {{key}} string replacement via load_email_template()</li>\n<li>Mobile-first via MJML default column stacking</li>\n<li>test_email query param on all blast endpoints for safety</li>\n<li>Browser preview for dev speed, real Gmail send for approval gate</li>\n</ul>","is_public":true,"note_type":"doc","status":null,"parent_slug":null,"position":null,"created_by_sub":null,"created_by_name":null,"updated_by_sub":null,"updated_by_name":null,"project":{"id":5,"name":"Westside Basketball","slug":"westside-basketball","platform":"github","repo_url":"https://github.com/ldraney/west-side-basketball","is_public":true,"page_note":null,"created_at":"2026-02-26T02:00:39","updated_at":"2026-03-15T18:39:02.426531"},"tags":[{"id":1,"name":"architecture"},{"id":8,"name":"active"}],"created_at":"2026-04-03T18:44:31.721598","updated_at":"2026-04-03T18:49:08.690649"}