Skip to content

Built email as a platform capability — three providers with failover, delivery webhooks, send and delivery logging, templating and campaigns — behind a startup check that will not boot without one.

Situation. Email carries a lot of weight on NextMariner — verification, notifications, digests, campaigns, the things a user actually waits for. And a mail provider is exactly the kind of dependency that fails quietly: the config looks fine, the app boots, and you only find out something’s broken when a real person never gets the message they were promised. That’s the worst way to learn about it. One provider makes it worse, because the failure is total and someone else’s to fix.

Task. Email had to be treated as a capability the platform owns rather than a client library it calls — able to survive a provider outage, able to say what happened to a given message, and loud at startup in the environments where silence is dangerous.

Action. Three providers sit behind one interface — SendGrid as primary, with SMTP2GO and Azure Communication Services behind it — and failover between them is automatic rather than a configuration change made under pressure. Delivery is not assumed: inbound webhooks report what each provider did with a message, and both sides are recorded, in a send log and a delivery event table, so “did this person get their verification mail” is a query rather than a guess. Templating keeps the message bodies out of the code, and a separate broadcast schema — 5 tables and 24 functions — carries campaigns to segments of users, which is a different problem from transactional mail and was built as one. In front of all of it, a startup check sends a real message through the stack, behind a flag: in development it logs a warning and carries on, because nobody wants their laptop refusing to start over an expired sandbox key, and in staging and production a failure is fatal and the process exits rather than deploy a build that cannot send mail. The send path itself goes through an SSRF‑protected client with a 30‑second timeout, and the async delivery path has retries and backoff so a momentary blip doesn’t drop a message.

Result. A whole category of silent failure moved from “a user notices days later” to “the deploy stops”, and a provider having a bad afternoon became a degraded path rather than an outage. The cost is three integrations to keep working instead of one, and delivery logs that grow and have to be pruned — both accepted, because email is the channel the platform cannot route around.