← Writing

Free professional email on your own domain: Cloudflare inbound, Resend outbound

Leaving Namecheap hosting, the one thing I couldn't figure out was my professional email. Here's how I got a full self-hosted email client — inbox, sending, storage — for $0 using Cloudflare Email Routing, Workers, and Resend.

On this page

When I decided to leave Namecheap hosting, everything had an obvious new home — static sites, apps, databases, storage — except one thing.

My professional email.

Every domain I own has addresses on it that clients and services actually use. Namecheap’s hosting bundle was quietly carrying all of it, and it was the one dependency I couldn’t route around. The obvious answers were all subscriptions: Google Workspace at $6 per user per month, Zoho, or just keep paying Namecheap. I was leaving to stop paying rent on infrastructure — signing a new lease on day one felt like defeat.

Then I found out Cloudflare will hand you raw email.

Inbound: Cloudflare Email Routing, but past the obvious part

Most people know Cloudflare Email Routing as free forwarding: mail to you@yourdomain.com lands in your Gmail. That alone replaces what Namecheap was charging for, and it’s genuinely free — no address limits that matter, on every domain you have on Cloudflare.

The less-known part is that a route’s destination doesn’t have to be another inbox. It can be a Worker. Cloudflare hands your code the full raw message — headers, body, attachments — and what happens next is up to you.

That changes the question entirely. You’re no longer choosing an email provider; you’re choosing what program receives your mail.

The program that receives my mail

I deployed Email Explorer, an open-source, full-stack email client by G4brym that runs entirely on a Cloudflare account — no server anywhere:

  • Workers run the app itself — API and a Vue web UI that looks and behaves like a normal mail client: folders, search, contacts, a rich-text composer, threading.
  • Durable Objects hold the mailboxes. Each mailbox is its own isolated object with its own SQLite database — messages, threads, and metadata live there.
  • R2 stores the attachments.

My routing rule sends every incoming email two places: into the Worker, where it’s parsed and saved permanently in my Cloudflare account, and a copy forwarded on to the personal inboxes I’ve set as forwarders. So I get a self-hosted archive I own and the convenience of mail still showing up where I already read it. If I ever stop forwarding, nothing is lost — the archive is the source of truth.

All of this — routing, Workers, Durable Objects, R2 at mailbox scale — sits inside Cloudflare’s free tier.

Then I tried to reply to an email.

The obstacle: replying costs $60 a year

Receiving mail in a Worker is free. Sending mail from a Worker via Cloudflare’s Email Sending requires the Workers paid plan — $5/month.

Five dollars is nothing, and also it’s everything: it’s $60 a year for the ability to hit reply, which is precisely the subscription-shaped arrangement I had just spent a weekend dismantling. The entire point of the migration was that every piece had to justify itself, and “pay annually to send email you could send free elsewhere” doesn’t.

Because sending email for free is a solved problem — just not at Cloudflare. Transactional email providers all have free tiers; Resend’s covers 3,000 emails a month from your own verified domain. I am not a newsletter. My reply volume doesn’t reach 3,000 in a year.

The two free tiers just needed to be connected.

The fix: fork it, make outbound pluggable

Upstream Email Explorer only knew one way to send — Cloudflare’s send_email binding. So I forked it and made the outbound path a pluggable provider. The Worker now decides at send time:

switch (settings?.provider) {
  case "resend":
    return sendViaResend(settings.apiKey, email);
  case "maileroo":
    return sendViaMaileroo(settings.apiKey, email);
  default:
    return sendViaCloudflare(env, email); // paid-plan path, if you have it
}

Each user configures their provider and API key in the client’s settings. Replies go out over Resend’s HTTP API instead of Cloudflare’s binding — same composer, same mailbox, different exit door. The part that took actual care was threading: a reply has to carry the right In-Reply-To and References headers or it shows up in the recipient’s inbox as a new conversation. The fork formats those identically across providers, so a reply sent via Resend threads exactly like one sent via Cloudflare.

Resend’s side is just DNS: verify the domain, add the SPF and DKIM records — which took two minutes, since DNS is Cloudflare too — and mail from me@mydomain.com arrives signed, aligned, and in the inbox rather than spam.

So the final shape is:

  • Inbound: Cloudflare Email Routing → Worker → Durable Objects + R2, copy forwarded to my inboxes. Free.
  • Outbound: Resend, from the same addresses, with correct threading. Free.
  • Total: $0, and I own every byte of my mail archive.

The caveats, honestly

Outbound deliverability is the one part of email you should never self-host, and this setup doesn’t — that’s the point of Resend. Sending SMTP from your own IP is a reputation war you lose before the first message; delegating to a provider whose business is inbox placement is the correct move at any budget.

A Cloudflare outage delays mail, it doesn’t eat it. Email is store-and-forward by design: if MX resolution or the Worker is briefly down, the sending server queues and retries — for days, per the RFCs. Short outages mean late mail, not lost mail. (Ask anyone whose registrar had a very bad day recently.)

This is personal-professional scale. A few domains, one human’s volume of correspondence. If you’re sending marketing campaigns or your business dies when an email bounces, buy the paid tier of something and get an SLA.

Free tiers are policy, not physics. Cloudflare or Resend could reprice tomorrow. The mitigation is the same pluggable-provider design that got me here: outbound is one settings change, and inbound is one MX record. Nothing about this setup is hard to leave — which, after Namecheap, was a design requirement.

Email was the last hostage my old hosting held. With it freed, the rest of the migration was just moving boxes — and that’s the next post.

← All writing Book a call →
Book a call → WhatsApp