[{"data":1,"prerenderedAt":390},["ShallowReactive",2],{"content-developer\u002Fdecisions\u002F005-custom-mta":3,"surround-\u002Fdeveloper\u002Fdecisions\u002F005-custom-mta":381},{"id":4,"title":5,"body":6,"description":373,"extension":374,"meta":375,"navigation":376,"path":377,"seo":378,"stem":379,"__hash__":380},"content\u002F3.developer\u002Fdecisions\u002F6.005-custom-mta.md","ADR-005: Custom MTA",{"type":7,"value":8,"toc":365},"minimark",[9,26,31,35,62,65,85,89,105,108,158,162,167,190,195,209,213],[10,11,12,20],"ul",{},[13,14,15,19],"li",{},[16,17,18],"strong",{},"Status:"," Accepted",[13,21,22,25],{},[16,23,24],{},"Date:"," 2025-03-20",[27,28,30],"h2",{"id":29},"context","Context",[32,33,34],"p",{},"Owlat previously depended entirely on AWS SES and Resend for email delivery. While these providers offer reliable APIs and managed infrastructure, they introduce constraints at scale:",[36,37,38,44,50,56],"ol",{},[13,39,40,43],{},[16,41,42],{},"Cost"," — per-email pricing scales linearly. High-volume senders pay significantly more than the cost of direct SMTP delivery from dedicated IPs.",[13,45,46,49],{},[16,47,48],{},"Deliverability control"," — third-party providers manage shared and dedicated IP reputation on behalf of many customers. Owlat has no ability to implement ISP-specific throttling, IP warming schedules, or engagement-based sending priority.",[13,51,52,55],{},[16,53,54],{},"Bounce latency"," — bounce and complaint data arrives via provider webhooks with variable delay, making it harder to react quickly to reputation issues.",[13,57,58,61],{},[16,59,60],{},"Rate limits"," — provider-imposed sending quotas (especially in SES sandbox) constrain campaign throughput. Owlat cannot independently manage backpressure per ISP.",[32,63,64],{},"The main options:",[36,66,67,73,79],{},[13,68,69,72],{},[16,70,71],{},"Keep SES\u002FResend only"," — simplest operationally, but cost and control limitations remain.",[13,74,75,78],{},[16,76,77],{},"Build a custom MTA"," — direct SMTP delivery with full control over IPs, throttling, warming, and bounce processing. Higher infrastructure complexity but significantly lower per-email cost and better deliverability tuning.",[13,80,81,84],{},[16,82,83],{},"Use an open-source MTA (Postfix, Haraka)"," — avoids building from scratch but requires adapting general-purpose software to Owlat's specific needs (GroupMQ integration, per-org circuit breakers, engagement priority).",[27,86,88],{"id":87},"decision","Decision",[32,90,91,92,96,97,100,101,104],{},"Build a custom MTA as a standalone service (",[93,94,95],"code",{},"apps\u002Fmta\u002F",") that sends email via direct SMTP delivery to recipient mail servers. The MTA is an ",[16,98,99],{},"optional"," email provider — selected via ",[93,102,103],{},"EMAIL_PROVIDER=mta"," — alongside the existing SES and Resend providers.",[32,106,107],{},"Key design choices:",[10,109,110,130,140,146,152],{},[13,111,112,115,116,119,120,119,123,119,126,129],{},[16,113,114],{},"Hono HTTP API"," — lightweight, standard HTTP server for receiving send requests from the Convex backend. Endpoints: ",[93,117,118],{},"\u002Fsend",", ",[93,121,122],{},"\u002Fsend\u002Fbatch",[93,124,125],{},"\u002Fhealth",[93,127,128],{},"\u002Fmetrics",".",[13,131,132,135,136,139],{},[16,133,134],{},"GroupMQ + Redis"," — job queue with group-based processing. Jobs are grouped by ",[93,137,138],{},"{ipPool}:{recipientDomain}"," so emails to the same ISP from the same IP pool are processed sequentially, respecting per-domain rate limits.",[13,141,142,145],{},[16,143,144],{},"Intelligence pipeline"," — six pre-send checks run before every delivery attempt: circuit breaker (per-org bounce protection), domain throttle (adaptive per-ISP rate limiting), SMTP response tracking, DNSBL checking, IP warming cap, and engagement-based priority.",[13,147,148,151],{},[16,149,150],{},"VERP return-path"," — Variable Envelope Return Path encoding correlates bounce messages back to original sends without maintaining a lookup table.",[13,153,154,157],{},[16,155,156],{},"Webhook feedback loop"," — delivery events (sent, bounced, complained) are posted back to the Convex backend via authenticated webhooks, reusing the existing bounce\u002Fcomplaint processing pipeline.",[27,159,161],{"id":160},"consequences","Consequences",[32,163,164],{},[16,165,166],{},"Enables:",[10,168,169,172,175,178,181,184,187],{},[13,170,171],{},"Per-ISP adaptive rate limiting (Gmail 100\u002Fmin, Outlook 80\u002Fmin, Yahoo 50\u002Fmin) with automatic backoff on 4xx responses",[13,173,174],{},"Automated IP warming over 30 days with adaptive acceleration\u002Fdeceleration based on deliverability signals",[13,176,177],{},"Engagement-based sending priority — high-engagement contacts are delivered first",[13,179,180],{},"Per-organization circuit breaker — automatically pauses sending when bounce rates exceed thresholds",[13,182,183],{},"DNS blocklist monitoring with automatic IP removal from active pool",[13,185,186],{},"Direct cost savings at scale (no per-email API fees beyond infrastructure)",[13,188,189],{},"Full bounce\u002Fcomplaint processing pipeline with DSN parsing and ARF\u002FFBL support",[32,191,192],{},[16,193,194],{},"Trade-offs:",[10,196,197,200,203,206],{},[13,198,199],{},"Requires dedicated IPs with proper rDNS\u002FPTR records and DKIM key management",[13,201,202],{},"Infrastructure complexity: Redis for state, SMTP port 25 access, separate containerized service",[13,204,205],{},"Operational burden: monitoring DNSBL listings, managing IP warming, investigating deliverability issues",[13,207,208],{},"Falls back gracefully to SES\u002FResend — the MTA is additive, not a replacement",[27,210,212],{"id":211},"comparison-with-alternatives","Comparison with Alternatives",[214,215,216,235],"table",{},[217,218,219],"thead",{},[220,221,222,226,229,232],"tr",{},[223,224,225],"th",{},"Capability",[223,227,228],{},"Owlat MTA",[223,230,231],{},"Postal",[223,233,234],{},"SES\u002FResend",[236,237,238,253,265,277,289,301,313,326,338,351],"tbody",{},[220,239,240,244,247,250],{},[241,242,243],"td",{},"Adaptive per-ISP throttling",[241,245,246],{},"Yes (10+ ISP profiles)",[241,248,249],{},"No",[241,251,252],{},"Provider-managed",[220,254,255,258,261,263],{},[241,256,257],{},"IP warming",[241,259,260],{},"Yes (30-day adaptive)",[241,262,249],{},[241,264,252],{},[220,266,267,270,273,275],{},[241,268,269],{},"Per-org circuit breaker",[241,271,272],{},"Yes (3-state)",[241,274,249],{},[241,276,249],{},[220,278,279,282,285,287],{},[241,280,281],{},"Engagement-based priority",[241,283,284],{},"Yes",[241,286,249],{},[241,288,249],{},[220,290,291,294,297,299],{},[241,292,293],{},"DNSBL auto-remediation",[241,295,296],{},"Yes (15-min checks)",[241,298,249],{},[241,300,252],{},[220,302,303,306,308,311],{},[241,304,305],{},"Message storage\u002Fsearch",[241,307,249],{},[241,309,310],{},"Yes (per-server MySQL)",[241,312,249],{},[220,314,315,318,320,323],{},[241,316,317],{},"Web admin UI",[241,319,249],{},[241,321,322],{},"Yes (Rails dashboard)",[241,324,325],{},"Provider console",[220,327,328,331,333,336],{},[241,329,330],{},"Spam content scoring",[241,332,249],{},[241,334,335],{},"Yes (pluggable inspectors)",[241,337,252],{},[220,339,340,343,346,349],{},[241,341,342],{},"Click\u002Fopen tracking",[241,344,345],{},"No (platform layer)",[241,347,348],{},"Yes (built-in)",[241,350,252],{},[220,352,353,356,359,362],{},[241,354,355],{},"Cost at scale",[241,357,358],{},"Low (infrastructure only)",[241,360,361],{},"Low (self-hosted)",[241,363,364],{},"High (per-email)",{"title":366,"searchDepth":367,"depth":367,"links":368},"",2,[369,370,371,372],{"id":29,"depth":367,"text":30},{"id":87,"depth":367,"text":88},{"id":160,"depth":367,"text":161},{"id":211,"depth":367,"text":212},"Why Owlat built a custom Mail Transfer Agent instead of relying solely on third-party email providers.","md",{},true,"\u002Fdeveloper\u002Fdecisions\u002F005-custom-mta",{"title":5,"description":373},"3.developer\u002Fdecisions\u002F6.005-custom-mta","ca6YaeQxxcNZYRkWRTlKgWoEoOYvSZfJ337oEVz-Th0",[382,386],{"title":383,"path":384,"stem":385,"children":-1},"ADR-004: Monorepo with Bun Workspaces","\u002Fdeveloper\u002Fdecisions\u002F004-monorepo-bun-workspaces","3.developer\u002Fdecisions\u002F5.004-monorepo-bun-workspaces",{"title":387,"path":388,"stem":389,"children":-1},"ADR-006: Self-Hosted Convex","\u002Fdeveloper\u002Fdecisions\u002F006-self-hosted-convex","3.developer\u002Fdecisions\u002F7.006-self-hosted-convex",1774391042824]