If you've tried sending email from a Cloudflare Worker, a Deno app, or a Bun server, you've probably hit the same wall I did: Nodemailer doesn't run there.
Nodemailer has been the default for ~15 years, and it's excellent — but it's built on Node's net/tls/stream built-ins. On the edge and on non-Node runtimes, those simply don't exist. You end up reaching for a provider's proprietary SDK per environment, or giving up on a unified API.
So I built sently — a runtime-agnostic email library that runs anywhere JavaScript runs.
Nodemailer vs sently
| Nodemailer | sently | |
|---|---|---|
| Runtimes | Node only | Node, Bun, Deno, CF Workers |
| Module format | CommonJS | ESM only |
| Bundle size | ~58 KB always | ~6 KB HTTP · ~15 KB SMTP |
| HTTP transports | via plugins | 6 built-in |
| React Email | via plugin | built-in |
| Idempotency | ✗ | ✓ |
| Webhook parsing | ✗ | ✓ |
| TypeScript | via @types | built-in |
The idea
One API, every runtime. sently uses only Web APIs (fetch, Web Crypto, Streams) in its core, so the same code sends mail on Node, Bun, Deno, and Cloudflare Workers. No Node built-ins in the hot path.
Sending via an HTTP provider
Most edge use cases use an HTTP API like Resend or SendGrid. Here's the whole thing:
import { createMailer } from "sently/mailer";
import { ResendTransport } from "sently/transports/resend";
const mailer = await createMailer({
transport: new ResendTransport({ apiKey: env.RESEND_API_KEY }),
});
await mailer.send({
from: "you@yourdomain.com",
to: "user@example.com",
subject: "Hello from the edge",
html: "<p>Sent from a Worker.</p>",
});
That exact code runs unmodified in a Cloudflare Worker, a Deno deploy, a Bun server, or plain Node. You only change the transport if you want a different provider.
Why it stays small
sently ships as subpath exports, so you only bundle what you import. A complete HTTP send path (mailer + Resend) is about 6 KB gzip.
For comparison, Nodemailer is ~58 KB min+gzip (measured on Bundlephobia) — and Node-only. So on top of running where Nodemailer can't, the HTTP path is roughly 10× smaller.
SMTP, DKIM, webhooks, idempotency, React Email rendering — all opt-in, none pulled in unless you import them.
Need SMTP instead?
HTTP providers are the common edge case, but classic SMTP relays work too — with pooling, STARTTLS, and DKIM:
import { createSMTPMailer } from "sently";
const mailer = await createSMTPMailer({
host: "smtp.example.com",
port: 587,
auth: { user: "you@example.com", pass: process.env.SMTP_PASS },
});
await mailer.send({
from: "you@example.com",
to: "user@example.com",
subject: "Hello",
html: "<p>Sent over SMTP.</p>",
});
(SMTP needs raw sockets, so it runs on Node, Bun, and Deno — not inside a Worker, which has no socket access. The HTTP transports above are the edge path.)
A nice local-dev touch
In development you can write emails to disk instead of sending them — no test inbox needed:
import { createMailer } from "sently/mailer";
import { PreviewTransport } from "sently/transports/preview";
const mailer = await createMailer({
transport: new PreviewTransport({ outDir: ".emails", open: true }),
});
What's in it
- HTTP transports: Resend, SendGrid, Postmark, Mailgun, AWS SES, Brevo
- SMTP: STARTTLS/TLS, pooling, DKIM (RSA + Ed25519), OAuth2 (Gmail + Microsoft 365)
- React Email rendering
- Idempotency — dedupe on retry/replay (handy for billing/notification emails)
- Bulk send with native provider batch endpoints
- Webhook parsing for all six providers, with constant-time signature verification
- ESM-native, tree-shakeable, TypeScript-first
Try it
bun add sently # npm / pnpm / deno / jsr too
It's pre-1.0 but stable — pin an exact version for production. I built it for my own projects and opened it up; feedback and issues are very welcome.
🔗 github.com/alialnaghmoush/sently
What are you using to send email from the edge right now? Curious whether others have hit the same Nodemailer wall — let me know in the comments.
Top comments (0)