npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@suluk/email

v0.3.0

Published

The missing EmailProvider binding + a pure renderEmailHtml(options)→HTML generator with a per-event/per-locale branded template set (verify/reset/change-email/delete/order-confirmation/order-status/newsletter). Emits content the app SENDS — never a hosted

Readme


CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.

Install

bun add @suluk/email

Strings flow through @suluk/i18n (a workspace dependency), so every template works out of the box in English and localizes — including RTL — when you supply a catalog.

What it does

  • A swappable EmailProvider binding. Pick one impl: consoleProvider (DEV — logs a summary, never touches the network) or resendProvider (a Workers-safe Resend binding over the REST API via fetch, no resend SDK). pickProvider({ dev, apiKey, from }) chooses between them. Providers never throw — a failed send returns { ok: false, error }.
  • A pure branded HTML generator. renderEmailHtml(options, ctx) is a deterministic options → HTML function. Colors, logo, name, and base URL are parameters (no hardcoded brand); the <html dir> and <lang> come from the locale.
  • A per-event template set. verifyEmail, resetPasswordEmail, changeEmailEmail, deleteAccountEmail, orderConfirmationEmail, orderStatusEmail, newsletterEmail — each returns a sendable { subject, html } you spread into a provider with a to.
  • Audience-sync. syncNewsletter reconciles your newsletter rows to a provider audience/list (subscribed → upsert, unsubscribed → remove) through a swappable AudienceProvider (consoleAudience / resendAudience).

When to reach for it

  • You need to send transactional or marketing email from a Suluk app and want the dev/prod provider switch, the Resend-over-fetch binding, and a branded localized template set handed to you.
  • You're on Cloudflare Workers and can't use the resend SDK — resendProvider is fetch-only.
  • You want email content (subject + HTML) as a pure value you can test, snapshot, and localize, decoupled from how it's sent.

Reach for @suluk/i18n directly when you only need string translation, and @suluk/theme for the design-token contract. This package is the email-specific layer that composes both.

Usage

Send through the provider binding

import { pickProvider } from "@suluk/email";

// dev ⇒ consoleProvider (logs, no network); prod ⇒ resendProvider over the REST API.
// Missing apiKey/from also falls back to the safe dev provider.
const provider = pickProvider({
  dev: process.env.NODE_ENV !== "production",
  apiKey: process.env.RESEND_API_KEY,
  from: "Acme <[email protected]>", // a verified Resend domain in prod
});

const result = await provider.send({
  to: "[email protected]",
  subject: "Hi",
  html: "<p>Hello.</p>",
});
// result: { ok: boolean; id?: string; error?: string; costMicroUsd?: number }

consoleProvider() and resendProvider({ apiKey, from }) can also be constructed directly. Both accept an injected fetch/log for tests, and resendProvider takes an optional costMicroUsd advisory for @suluk/cost metering.

Render a branded message from a template

Each template takes its params plus a TemplateContext ({ brand, messages?, dir?, lang?, year? }) and returns a { subject, html } you spread into a send:

import { verifyEmail, pickProvider, type EmailBrand } from "@suluk/email";

const brand: EmailBrand = { brandName: "Acme", baseUrl: "https://acme.com" };

const message = verifyEmail(
  { verifyUrl: "https://acme.com/verify?token=abc", userName: "Sam" },
  { brand },
);

await pickProvider({ dev: false, apiKey, from }).send({
  to: "[email protected]",
  ...message, // subject + html
});

The auth lifecycle (verifyEmail, resetPasswordEmail, changeEmailEmail, deleteAccountEmail) plugs straight into Better Auth's sendVerificationEmail / sendResetPassword hooks.

Ecommerce + marketing templates

import { orderConfirmationEmail, newsletterEmail } from "@suluk/email";

const order = orderConfirmationEmail(
  {
    orderNumber: "1042",
    items: [{ name: "Widget", qty: 2, totalCents: 3998 }],
    totalCents: 3998,
    currency: "USD",
    locale: "en-US",                 // amounts formatted via Intl
    shippingAddress: ["Jane Doe", "12 Oak St", "Austin, TX 78701", "US"],
    orderUrl: "https://acme.com/orders/1042",
  },
  { brand },
);

const news = newsletterEmail(
  { subject: "June update", heading: "What's new", bodyHtml: "<p>…</p>", unsubscribeUrl: "https://acme.com/u/abc" },
  { brand },
);

Pure render, custom brand + locale

renderEmailHtml is the generator the templates wrap — call it directly to build your own message body. Brand accents are parameters and the document direction comes from the locale:

import { renderEmailHtml } from "@suluk/email";

const html = renderEmailHtml(
  { icon: "✉", heading: "مرحبا", body: "<p>…</p>", ctaLabel: "تأكيد", ctaUrl: "https://acme.com/c" },
  {
    brand: { brandName: "Acme", baseUrl: "https://acme.com", accentFrom: "#0066ff", accentTo: "#3399ff" },
    messages: { didNotRequest: "لم تطلب هذا؟" }, // overrides English defaults (DEFAULT_EMAIL_STRINGS)
    dir: "rtl",
    lang: "ar",
  },
);

Sync the newsletter audience

import { resendAudience, syncNewsletter, type NewsletterRow } from "@suluk/email";

const rows: NewsletterRow[] = [
  { email: "[email protected]", status: "subscribed" },
  { email: "[email protected]", status: "unsubscribed" },
];

const audience = resendAudience({ apiKey: process.env.RESEND_API_KEY! });
const tally = await syncNewsletter(audience, "aud_123", rows);
// tally: { upserted: number; removed: number; failed: number }

syncNewsletter drives the audience from your DB rows (the source of truth) so the two never drift. Use consoleAudience() in dev.

API

| Export | What it does | | --- | --- | | pickProvider(opts) | dev ⇒ consoleProvider, prod ⇒ resendProvider; safe fallback when key/from missing | | consoleProvider(opts?) | DEV EmailProvider — logs a summary, never sends | | resendProvider(opts) | Workers-safe Resend EmailProvider over the REST API (fetch, no SDK) | | renderEmailHtml(options, ctx) | pure branded-HTML generator (the email analogue of render-form) | | DEFAULT_EMAIL_STRINGS | English fallbacks for the chrome strings (footer, "did not request") | | verifyEmail / resetPasswordEmail / changeEmailEmail / deleteAccountEmail | auth-lifecycle templates → { subject, html } | | orderConfirmationEmail / orderStatusEmail / newsletterEmail | ecommerce + marketing templates | | TEMPLATE_STRINGS | the full English string catalog the templates use | | consoleAudience / resendAudience | swappable AudienceProvider impls (dev / Resend Audiences) | | syncNewsletter(provider, audienceId, rows) | reconcile NewsletterRow[] → audience, returns SyncResult |

Key types: EmailProvider, EmailMessage, SendResult, ConsoleProviderOptions, ResendProviderOptions, EmailBrand, BrandedEmailOptions, RenderContext, TemplateContext, OrderLine, AudienceProvider, AudienceContact, AudienceResult, ConsoleAudienceOptions, ResendAudienceOptions, NewsletterRow, SyncResult.

Boundary

This package renders and sends — it never hosts a mailer. resendProvider is a thin binding to an external service (like a rate-limit KV binding), not a Suluk-hosted mail service: it just fetches the Resend REST API. The send mechanism and the dev/prod switch live here; what stays app-side is your own branding, copy, and the { to, from } addressing you spread in — and the API key, which you inject (pulled from @suluk/env and passed through, since on Workers the secret comes from the Worker env, not process.env). Templates are pure values: deterministic, testable, localized via an injected @suluk/i18n catalog.

License

Apache-2.0.