Messaging that runs everywhere your code does

Email, SMS, WhatsApp, and Web Push — one channel-first API for Node.js, Bun, Deno, and Cloudflare Workers. Zero runtime dependencies.

Get started

Runs on Node · Bun · Deno · Cloudflare Workers

import { createMailer } from "sently/mailer";
import { ResendTransport } from "sently/transports/resend";

const mailer = await createMailer({
  transport: new ResendTransport({ apiKey: process.env.RESEND_API_KEY! }),
});

await mailer.send({
  from: "hello@example.com",
  to: "you@example.com",
  subject: "Welcome",
  html: "<p>Sent with sently.</p>",
});
Swap the transport — app code stays on sently.
transports
  • Resend
  • SendGrid
  • Postmark
  • Mailgun
  • AWS SES
  • Brevo
  • SMTP
  • Cloudflare Email
  • SparkPost
  • MailerSend
  • Loops
  • Plunk
  • Mailtrap
  • SNDR
  • Twilio
  • Taqnyat
  • Msegat
  • Unifonic
  • WhatsApp Cloud
  • Web Push
  • FCM
readme

Apps use sently channel senders, not vendor SDKs. Providers are transports under those senders — swap Resend for SES, or Twilio for Taqnyat, without rewriting your call sites.

Tree-shakeable subpaths keep HTTP email around ~6 KB gzip. SMTP, DKIM, OAuth2, pooling, plugins, idempotency, and webhook parsers ship in the same package.

$ bun add sently
email
sently/mailer · sently/smtp
channels
sently/sms · sently/whatsapp · sently/push
why

A library, not a platform

No dashboard, no per-send pricing, no vendor lock-in. Define senders in code, plug a transport, and let the type system keep call sites honest across every runtime.

  • Channel-first

    Apps call createMailer / createSmsSender — not vendor SDKs. Swap the transport; call sites stay put.

  • Pluggable transports

    One provider per subpath. Resend today, SES tomorrow — same sender contract.

  • Every runtime

    Node, Bun, Deno, and Cloudflare Workers from one ESM package. Socket adapters where SMTP needs them.

  • Tree-shakeable

    Import sently/mailer for HTTP email (~6 KB gzip). SMTP, SMS, WhatsApp, and push stay out until you need them.

  • Composable path

    Retry, fallback, weighted failover, and idempotency wrap any transport without a new API.

  • Stable errors

    SentlyError with codes like RATE_LIMITED and BAD_REQUEST — catch by code, not string matching.

ConcernTypical stacksently
RuntimesNode onlyNode · Bun · Deno · CF Workers
ChannelsEmail (+ separate SDKs)Email · SMS · WhatsApp · Push
ProvidersVendor clients in app codePluggable transports
pipeline

One flow. Every channel.

Every send follows the same path: pick a channel sender, pass a transport, call send. Hooks observe; decorators strengthen the path without a new API.

  1. 01
    SendercreateMailer · createSmsSender · createWhatsAppSender · createPushSender
  2. 02
    TransportResend · SMTP · Twilio · WhatsApp Cloud · Web Push — or your own
  3. 03
    Sendmailer.send() · sms.send() — same shape, channel-specific options
  4. 04
    ResultmessageId · accepted · provider response — stable error codes
Hooks

Observe without changing the send — onSend, onSuccess, onError, onRetry. No body or PII in hook context.

Decorators

Wrap any transport — RetryTransport, FallbackTransport, IdempotencyTransport. Same contract, stronger send path.

channels
entrypoints

Import only what you need

HTTP email from sently/mailer. SMTP from sently/smtp. SMS, WhatsApp, and push from their own subpaths. Each transport is a separate entry so unused providers stay out of the bundle.

sently/mailer
Custom / HTTP email transports
sently/smtp
Host, pool, adapters, DKIM
sently/sms
SMS channel sender
sently/whatsapp
WhatsApp channel sender
sently/push
Web Push channel sender
sently/transports/*
One provider per subpath
start here

Learn the channel model once

Sender → transport → send. The same shape for every channel; only the options and providers change.