@rbrowton/email
v0.1.2
Published
Shared transactional email for the People Work Life portfolio
Readme
@rbrowton/email
Shared transactional email for the People Work Life portfolio: escaping and layout primitives, a brand/theme/labels system so each app can look like itself, a provider layer with an honest console fallback, and five ready-made templates (welcome, password reset, magic link, invite, digest) plus generalised Supabase Auth email templates. It has zero runtime dependencies, is ESM/strict-TypeScript throughout, and is built to be dropped into any app in the portfolio without pulling in a framework.
Install
pnpm add @rbrowton/emailDefine a brand
Brands are built with defineBrand and passed explicitly to every template and
primitive — nothing is cached at module scope, so an app that serves more than
one tenant can resolve a different brand per request.
import { defineBrand } from "@rbrowton/email";
const brand = defineBrand({
identity: {
name: "WisdomWeave",
from: "WisdomWeave <[email protected]>",
siteUrl: "https://wisdomweave.app",
legalFooter: "WisdomWeave, part of People Work Life Ltd.",
},
theme: { accent: "#2A6F4A" },
});Send an email
import { sendEmail, welcome } from "@rbrowton/email";
const { subject, html, text } = welcome({ name: "Alex", ctaUrl: "https://wisdomweave.app/start" }, brand);
const result = await sendEmail({ to: "[email protected]", subject, html, text });
if (!result.ok) {
// result.reason is "not_configured" | "send_failed" | "invalid".
// "not_configured" means RESEND_API_KEY is unset — the console provider
// logged the message locally (or, in production, withheld it) and nothing
// was sent. Handle this explicitly: fall back to a copy-link flow, surface
// an error to an admin, whatever fits the caller. Do not treat it as success.
console.error("email not sent:", result.reason, result.detail);
}Preview templates in development
createPreviewHandler renders every template against sample data behind a
NODE_ENV !== "production" guard, so it is safe to leave mounted. Import it
from the /preview subpath — it is not part of the main entry point, so it
never ships to a production bundle by accident.
// app/dev/emails/[[...slug]]/route.ts
import { createPreviewHandler } from "@rbrowton/email/preview";
const handler = createPreviewHandler({ brand });
export const GET = handler;Things worth knowing
ConsoleEmailProvider, which is selected automatically whenever RESEND_API_KEY
is unset, never reports success for mail it did not send — it always returns
{ ok: false, reason: "not_configured" }, whether or not it logged the message
to the console. There is no synthetic success path.
Every template returns a plain-text text part alongside html, generated
automatically with toPlainText. You never need to write one by hand, and
HTML-only mail is never sent.
What this is not
This package renders and sends individual transactional emails. It is not a marketing or broadcast email tool — no lists, no campaigns, no unsubscribe management, no analytics. For anything in that territory, use a dedicated service.
