Why I Built My Own Transactional Email Service (and Replaced Resend Across 12 Projects) | CommVergent Automation
I run a portfolio of SaaS products under CommVergent Automation. BildOut for invoicing. Elysian Money for personal finance. FigrOut for project accounting. MerchIQ for e-commerce. HackHunters for cybersecurity. A handful of others. Twelve projects in total, each with its own sending domain, its own email needs, and — until recently — its own Resend account.<br>That last part was the problem.<br>The credential sprawl problem<br>Every project that sends email needs an account with an email service provider. For me, that meant twelve sets of API keys across multiple Resend accounts, each configured independently. Twelve dashboards to check delivery rates. Twelve DKIM records to manage. Twelve places where a forgotten password rotation could silently break password reset emails for real users.<br>It wasn't a cost problem — Resend's pricing is fair. It was an operational problem. I couldn't remember which credentials belonged to which project. I had no consolidated view of what was being sent across all my platforms. And when MerchIQ needed to provision custom sending domains for tenants, I was doing it manually through Resend's UI for each one.<br>For a solo developer running everything, this kind of overhead compounds. Every hour spent managing email infrastructure is an hour not spent building product.<br>What I already had<br>I wasn't starting from zero. My on-premises mail infrastructure already included a two-node Linux mail cluster running Postfix, Dovecot, and rspamd — handling inbound mail forwarding for six domains and hosting mailboxes for macmillancloud.com. I had a working relationship with SocketLabs as my SMTP relay, with established IP reputation and DKIM signing. The plumbing was there. What I needed was an application layer on top of it.<br>What I built<br>MailWain is a thin transactional email API service. The architecture is deliberately simple:<br>A NestJS API running on Fly.io accepts email send requests via REST API or SMTP, validates the sender domain against a per-organization registry, queues the message through BullMQ/Redis, and injects it into SocketLabs for delivery. A Next.js admin dashboard on Vercel provides domain management, delivery logs, suppression lists, usage metering, and organization management.<br>The key design decision was multi-tenancy from day one. Each of my projects is an "organization" in MailWain with its own API key, its own registered sending domains, its own usage tracking, and its own rate limits. The data model doesn't distinguish between my internal platforms and hypothetical future external customers — they're all organizations. Row-Level Security in Supabase enforces isolation at the database level, not just the application layer.<br>Domain provisioning is automated. When a project needs a new sending domain, the API generates a 2048-bit RSA DKIM key pair, registers the domain with SocketLabs, and returns the DNS records to publish. After DNS propagates, a verify call provisions DKIM signing in SocketLabs and activates the domain. The whole flow takes minutes, not the half hour of manual dashboard clicking it used to require.<br>For Supabase integration — which most of my projects use for authentication — MailWain exposes an SMTP submission endpoint with a valid Let's Encrypt certificate. Each Supabase project points its Custom SMTP settings at MailWain, using the organization's API key as the username and API secret as the password. Auth emails (signup confirmations, password resets, magic links) flow through the same pipeline as application emails, with the same delivery tracking and suppression management.<br>The migration<br>Migrating twelve projects sounds daunting. It wasn't. Each project followed the same pattern:<br>Create an organization in the MailWain admin dashboard<br>Register the sending domain and add DNS records<br>Create a lightweight HTTP client in the project (a 40-line wrapper around fetch)<br>Replace every resend.emails.send() call with the new client<br>Remove the Resend SDK dependency<br>Update environment variables<br>Deploy<br>Most projects took about 30 minutes. The simpler ones — a single API route sending a newsletter or a contact form confirmation — were done in 15. The templates didn't need to change at all. React Email components render to HTML strings, which MailWain accepts as-is. Plain-text templates pass through as the text field. Same HTML, different transport.<br>BildOut was the most complex migration. It had eight separate sending routes (invoices, payment receipts, team invitations, overdue reminders, scheduled reports), a custom domain management layer for tenants, and a delivery webhook system that tracked bounces and complaints for domain health monitoring. The Resend SDK was deeply embedded. Even so, the migration was a matter of swapping the transport layer — the business logic, templates, and sending rules stayed the same.<br>MerchIQ was the most interesting migration...