CodingGroup
field-notes

Transactional Email on a Free Tier: Brevo, Eight DNS Records, and the MX Record You Must Not Touch

We needed one address that could both receive customer mail and send replies from our own domain, without paying for a mailbox provider. The setup is Brevo's free tier, Cloudflare Email Routing on the inbound side, and a Worker in between. Eight DNS records did the job. One of the instructions in the Brevo dashboard would have silently broken every inbound email, and it is the same record every guide tells you to add.

The short version: one domain, one address (service@), inbound mail routed by Cloudflare, outbound mail sent through Brevo’s free tier (300 emails/day), and a Worker that holds every draft until a human approves it. The whole thing costs nothing per month and took about an hour of DNS work. Eight records were needed, and the eighth is the one that makes receiving and sending coexist on a single domain. The dashboard’s own instructions include one that would have killed inbound mail on that address, and nothing in the setup flow warns you about the conflict.

This is the configuration, the reasoning behind each record, and the trap.

What we actually needed

The requirement was narrower than “email infrastructure”:

  • Receive mail at [email protected] and have it land in a system we control, not a shared inbox;
  • Reply from that same address, so the customer sees one consistent identity;
  • No marketing blasts — this is transactional mail: acknowledgements, replies, notifications;
  • No monthly fee, because the volume is a handful of messages a day.

That combination rules out most of the obvious answers. A mailbox provider gives you receive-and-reply but charges per seat. A marketing platform gives you sending but treats every message as a campaign and wants a list. What we wanted was the plumbing underneath a support address.

The architecture is three pieces, not one

inbound:   sender → Cloudflare Email Routing → Worker → database
outbound:  Worker → Brevo API → recipient

Cloudflare owns the inbound path because it already hosts DNS. Brevo owns the outbound path because it does authenticated sending well and gives away 300 messages a day. The Worker is the only piece we wrote, and its job is not to send mail — its job is to make sending a deliberate act.

That separation matters more than it sounds. If the Worker called Brevo directly when a form was submitted, a bug or a spam wave would burn the daily quota or, worse, damage the sending reputation of a domain we intend to keep for years. Instead, a submission creates a draft. A human approves it. A separate endpoint flushes approved drafts, and a scheduled trigger picks up anything the flush missed.

Four states, one gate: pending → approved → sending → sent. Two details make it safe under real traffic. The transition into sending is a conditional update — WHERE status = 'approved' — so two flush runs that overlap cannot both claim the same message. And anything stuck in sending for more than ten minutes is rolled back to approved rather than lost, which turns the failure mode from “silently dropped email” into “sent a few minutes late”.

The seven DNS records, and what each one does

The DNS work is the whole setup. Everything else is a form.

# Type Name Purpose
1 TXT @ SPF: v=spf1 include:_spf.mx.cloudflare.net include:spf.brevo.com
2 TXT @ brevo-code:… — proves you control the domain
3 CNAME brevo1._domainkey DKIM key 1
4 CNAME brevo2._domainkey DKIM key 2 (rotation / redundancy)
5 TXT _dmarc v=DMARC1; p=none; rua=mailto:[email protected]
6 CNAME mail branded link domain
7 CNAME r.mail branded click-tracking domain
8 CNAME img.mail branded image-hosting domain

The SPF record is the one people get wrong when a domain does both jobs. It has to authorize both senders — in our case Cloudflare (which forwards inbound mail and therefore re-sends it) and Brevo (which sends replies). Publishing only Brevo’s include is enough for your outbound replies and quietly marks every forwarded message as unauthorized, which is exactly the kind of partial failure that shows up as “some mail is landing in spam” rather than as a clear error.

The first five are the ones that decide whether your mail is delivered. Records 5–7 are cosmetic in the strict sense — the mail arrives without them — but they change what a recipient sees when they hover a link. A tracking URL on your own subdomain reads as a company; a tracking URL on a third-party domain reads as bulk mail, and some corporate filters treat it that way.

DKIM gets two keys rather than one so that a key can be rotated without a window where nothing is signed. Two CNAMEs are cheap insurance.

DMARC starts at p=none deliberately. p=none means “report, do not enforce”. For the first weeks of a new sending domain you want the reports, not the rejections, because the reports are how you discover that some internal tool is sending as your domain without DKIM. Moving to p=quarantine is a decision for later, once the reports are boring.

The trap: the MX record you must not add

Brevo’s domain setup will, depending on which flow you follow, suggest adding an MX record pointing at Brevo. If you add it, mail addressed to [email protected] stops arriving at Cloudflare and starts arriving at Brevo — which is fine only if Brevo is also your mailbox, and it is not.

The failure mode is nasty because it is silent in the direction that matters. Outbound mail keeps working. Your sends still deliver, the dashboard still shows activity, and everything looks healthy. What breaks is inbound: customer replies, verification emails, password resets, and any message a human sends to that address simply stop appearing where your application expects them.

For us the correct answer was to touch nothing MX-related. Cloudflare Email Routing owns the MX records for the apex domain, and it must keep owning them. Brevo is a sender in this design, not a receiver. The eight records above are all TXT and CNAME precisely because none of them can redirect inbound mail.

The general rule: on a domain that both receives and sends, every record you add should be provably incapable of rerouting mail. If a setup guide asks for an MX change and you already have an inbound provider, stop and read what you are about to overwrite.

Why the Worker holds drafts instead of sending

The most useful design decision had nothing to do with Brevo.

Approach Failure mode
Send immediately on submit A bug emails a customer twice. A spam wave burns the quota. No one sees the content before it leaves.
Send from a script with the API key in it Key ends up in a repo. Any machine can send as your domain.
Draft → approve → flush A human sees the exact text and recipient first. Sending is an explicit action.

The API key lives as a Worker secret, never in code and never in the client. The flush endpoint requires an admin token separate from the key. And because anything worth automating eventually gets forgotten, a scheduled trigger runs every five minutes and sends whatever was approved but not yet flushed — the backstop, not the primary path.

This is the same argument as the draft-gate on any outbound system, and it is worth stating plainly: the expensive mistake in outbound mail is not a delivery failure, it is a correct delivery of the wrong message to the wrong person. A state machine with one human checkpoint costs a few seconds per message and removes that entire class of incident.

What the free tier actually gets you

Brevo’s free plan covers 300 emails per day, which for a one-person operation answering inquiries is roughly two orders of magnitude more than needed. The constraint that matters is not volume, it is classification: transactional and marketing mail are treated differently, and a system that sends customer replies is not the same thing as a system that sends newsletters. Keep them on separate tracks or the marketing rules will start applying to your support mail.

Two other limits worth knowing before you build on it:

  • Daily quota resets, it does not accumulate. A quiet Monday does not buy you a busy Tuesday.
  • Deliverability is a reputation you build, not a setting you enable. A new domain sending ten messages a day from a properly signed setup is in a good position. The same domain suddenly sending two thousand is not, regardless of how correct the DNS is.

The transferable part

If you strip away the vendor names, the pattern is:

  1. Separate receiving from sending. They are different problems with different DNS, and conflating them is how people break their own inbox.
  2. Never let the sending path be silent. Draft, approve, flush.
  3. Treat every DNS record as capable of breaking something. Ask what it can reroute before you add it.
  4. Start DMARC permissive and read the reports. Enforcement is a later decision, made with data.

The setup itself took an hour. Knowing which record not to touch took longer, and it is the part no dashboard will tell you.


Corrections and topic requests: contact page. A real person reads every message.