Skip to content

Cross-runtime email library

Upyo/uphjo/

Send email from anywhere your code runs.

Upyo is a small email library for JavaScript and TypeScript. You write a message once, choose a transport, and send. The same code works on Node.js, Deno, Bun, and edge functions, with whichever provider you prefer.

send.tsMailgun
import { createMessage } from "@upyo/core";
import { MailgunTransport } from "@upyo/mailgun";
const message = createMessage({ from: "[email protected]", to: "[email protected]", subject: "Hello from Upyo!", content: { text: "This is a test email." }, });
const transport = new MailgunTransport({ apiKey: process.env.MAILGUN_KEY, domain: process.env.MAILGUN_DOMAIN, });
const receipt = await transport.send(message);
Runs onNode.jsNode.jsDenoDenoBunBun Edge functions Cloudflare WorkersVercelNetlify

Runtimes

One API, every runtime

Most email libraries quietly assume Node.js. Upyo doesn't. It builds on web-standard APIs like fetch(), Uint8Array, File, and AbortSignal, so the same code runs unchanged across runtimes. You can develop against a local server and deploy to an edge function without rewriting anything.

Node.js
Node.js

The familiar default, with SMTP and every HTTP provider.

Deno
Deno

Install straight from JSR, with no build step required.

Bun
Bun

Runs on Bun's fast runtime without any shims.

Cloudflare WorkersVercelNetlify
Edge functions

Cloudflare Workers, Vercel, Netlify, Deno Deploy, and the like.

Read why Upyo exists

Transports

Switch providers without rewriting your app

A transport is simply where your messages go. Upyo gives every provider the same interface, so moving from SMTP in development to a hosted API in production, or from one provider to another, is a one-line change. The code that builds your message never has to know the difference.

transport.ts
// In development, talk to a local SMTP server:
const transport = new SmtpTransport({
  host: "localhost",
  port: 1025,
});

// In production, switch to a hosted API. Just this one
// line changes; everything below stays the same:
const transport = new SendGridTransport({
  apiKey: process.env.SENDGRID_KEY,
});

await transport.send(message);

Packaging

Modern from the build out

Upyo is written in TypeScript and published the way you'd hope a library written today would be. The result is a package that drops cleanly into whatever toolchain you already have, and a sending result the compiler can check for you.

receipt.tsType-safe
const receipt = await transport.send(message);

if (receipt.successful) {
  receipt.messageId;      // string, available only here
} else {
  receipt.errorMessages;  // string[], available only here
}
npm and JSR

Install from npm with the tools you already use, or from JSR for Deno. Both registries are first-class, not an afterthought.

ESM and CommonJS

Every package ships both module formats with type declarations, so it works whether your project uses import or require().

TypeScript-first

Types live in the source, so there are no @types packages to add. Results come back as a discriminated union you can narrow safely.

Zero dependencies

The core and transports lean on web-standard APIs instead of a dependency tree, so installs stay small and audits stay short.

Add Upyo to your project

Testing & telemetry

Built to be developed against

Two small things make day-to-day work easier: a mock transport for your tests, and OpenTelemetry for when you need to see what's happening in production. Both implement the same interface as every other transport, so they slot in without touching your sending code.

Test without sending real email

The mock transport keeps every message in memory so your tests can inspect exactly what would have been sent: recipients, subject, attachments, and all.

mock.test.ts
const transport = new MockTransport();
await transport.send(message);

const sent = transport.getSentMessages();
sent[0].subject;                // "Hello from Upyo!"
sent[0].recipients[0].address;  // "[email protected]"
Mock transport

See what's happening in production

Wrap any transport to get traces and metrics like delivery rates, latency, and error classification, with nothing to change in the code that actually sends.

observe.ts
const transport = createOpenTelemetryTransport(base, {
  serviceName: "email-service",
  metrics: { enabled: true },
  tracing: { enabled: true },
});

// your sending code stays the same
await transport.send(message);
OpenTelemetry transport

Get started

Ready to send your first message?

Add a package, copy one of the examples above, and you'll be sending email in a few minutes.

Upyo is free, open source, and maintained by Hong Minhee. If it saves you some time, you can support its continued development through GitHub Sponsors . Thank you for considering it.

Upyo (郵票) is the Sino-Korean word for a postage stamp.