v1.2.2open source · ESM-only · zero runtime deps

Notification infrastructure that runs everywhere your code does

One TypeScript API for Email, SMS, WhatsApp, and Push. Write once — switch providers without rewriting app code.

One package — sentlysently/mailer · sently/sms · sently/whatsapp · sently/push

Get started
  • Runs on
  • Node
  • Bun
  • Deno
  • Workers
email·createMailer
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.

Animated code tour cycling through email, SMS, WhatsApp, and push snippets to show the same sender shape across every channel. Hover or focus the panel to pause.

transports
  • live verified
  • available
  • sponsors soon
  • Resend
  • SendGrid
  • Postmark
  • Mailgun
  • AWS SES
  • Mailpit (live verified)
  • Inbucket (live verified)
  • SNDR (live verified)
  • Hostinger (live verified)
  • Taqnyat (live verified)
  • Web Push (live verified)
  • Brevo
  • SMTP
  • Cloudflare Email
  • SparkPost
  • MailerSend
  • Loops
  • Plunk
  • Mailtrap
  • Twilio
  • Msegat
  • Unifonic
  • WhatsApp Cloud
  • FCM
readme

Channel senders, not vendor SDKs

Apps call createMailer, createSmsSender, createWhatsAppSender, or createPushSender. Providers are transports under those senders.

$ bun add sently
email
sently/mailer · sently/smtp
channels
sently/sms · sently/whatsapp · sently/push
0
channels, one sender shape
0
provider transports
0
runtime dependencies
~0 KB
HTTP email stack, gzip
why

Why teams outgrow a single SDK

The pain is not sending once — it is keeping call sites stable as channels and providers change.

  • Different SDKs everywhere

    Every provider ships its own client. Sently exposes one sender shape per channel instead.

  • Per-vendor error shapes

    Catch SentlyError codes like RATE_LIMITED — not a different string format per vendor.

  • Reliability bolted on later

    Retry and fallback wrap any transport. Same decorators when you add the next channel.

  • Runtime lock-in

    One ESM package on Node, Bun, Deno, and Cloudflare Workers — including SMTP where sockets need adapters.

Typical stack — an SDK per channel

Every vendor ships its own auth, retries, and error strings into your codebase.

With sently — one sender layer

Senders own the contract; transports plug in underneath. Failures arrive as one shape.

A library, not a platform. Preference centers, digests, and workflow builders (custom or Novu, Knock, Courier) sit on top of sently — not instead of it.

pipeline

App → channel sender → transport

Pass a provider transport into the sender, then call send. Hooks observe; decorators add retry and fallback 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
budgets

Measured, not marketed

Four hard caps CI fails on — HTTP stack, SMTP, SMS, and push — then channel entrypoints and sently/transports/* as pickable imports. Every figure is from bundle-size-budgets.json.

CI budgetshard caps · measured from bundle-size-budgets.json
  • HTTP

    6.8 KB

    of 6.8 KB·100% used

    mailer + Resend · gzip

  • SMTP

    15.8 KB

    of 15.8 KB·100% used

    createSMTPMailer · gzip

  • SMS

    0.8 KB

    of 0.8 KB·100% used

    createSmsSender · gzip

  • Push

    1.4 KB

    of 1.4 KB·100% used

    createPushSender · gzip

import a stack · or pickgzip · vs smtp ceiling
mailer + ResendTransportbaseline · typical stack
6.8 KB
  • sently/sms0.8 KB5%
  • sently/whatsapp0.8 KB5%
  • sently/push1.4 KB9%
  • sently/mailer2.8 KB18%
  • sently2.8 KB18%
  • mailer + SNDR4.5 KB28%
  • sently/smtp15.8 KB100%
sently/transports/*gzip · vs smtp ceiling

CI-enforced gzip ceilings · isolated imports · unused transports stay out

start here
ship

$ bun add sently

Send your first message in five minutes

Install sently, pick a channel sender, pass a transport. Five lines to the first send.

Get started