bitbaum Start a project
Package · MIT

mail-kit

One email layer for every app. Never throws — it returns whether the mail was sent.

Why it exists

Email is where products quietly grow a second, private implementation: a different sender, a swallowed error, a test message that reaches a real person. 23 apps had started.

How it works

Resend, over raw fetch, with no other dependencies. The call returns a result that says whether the mail went, rather than throwing and hoping a catch exists. Sandbox and placeholder addresses are refused, and the shared sender is one convention.

Where it fits

Hire, notifications, and anything that has to reach a person outside the product. It is the boring half of 'reach someone'. threadkit is the half that stays inside.

LicenceMIT
Registry releasev0.2.0
Install
Sourcebitbaum/mail-kit
Declared consumers23 repositories

API and examples

Documentation from source commit d799d01a5233 · 2026-11-06. Source documentation can include work beyond the registry release.

Read the package documentation

The fleet's one email layer. One provider (Resend), one env contract, one call shape, zero runtime dependencies.

Exists because the fleet grew ~12 bespoke email implementations across three provider styles, and a dead SMTP credential silenced two apps' outbound mail for months without anyone noticing. Uniformity is the fix.

Install

pnpm add @bitbaum/mail-kit

Use

import { sendMail, isMailConfigured, mailHealth, conventionalFrom } from "@bitbaum/mail-kit";

const result = await sendMail({
  to: "person@example.ch",
  subject: "Your invoice",
  html: "<p>…</p>",
  attachments: [{ filename: "invoice.pdf", content: pdfBuffer, contentType: "application/pdf" }],
});

if (!result.sent) {
  // result.error, result.status?, result.retryable — sendMail NEVER throws.
  logger.error("invoice email failed", result);
}
  • sendMail never throws. A thrown email error inside a server action

reaches nobody and destroys the form; a { sent: false } you must look at is deliverable. Callers where delivery matters check .sent.

  • isMailConfigured() — gate flows that depend on an email arriving

(password reset, invoices). A placeholder key and a production sandbox sender (@resend.dev) count as unconfigured: both look configured and deliver to nobody.

  • mailHealth() — key-validity + domain-status probe for /api/health.

A passing probe does not prove delivery; only a real send does.

  • options.idempotencyKey — pass from crons/queues so a retried job

cannot double-send (Resend dedupes for 24h).

One address per app: hello@<host>

Every app has one address, on its own host, that a customer can guess: hello@<app>.orangecat.ch on the box, and hello@murphys-bar.ch once a site has its own domain. appAddress(url) builds it; appAddressFromEnv() reads MAIL_ADDRESS or derives it from APP_URL / NEXT_PUBLIC_SITE_URL / AUTH_URL.

sendMail puts that address in Reply-To on every message (unless the call sets its own, or already sends from it). Mail still leaves through the shared sender until each host can sign its own — but every answer reaches the app.

Mail to an app

The fleet's mail gateway receives for every app host, checks SPF/DKIM/DMARC, and POSTs an InboundMail to /api/mail/inbound, signed with MAIL_INBOUND_SECRET (x-mail-signature: t=…,v1=hmac). The app side is one line:

// app/api/mail/inbound/route.ts
import { inboundHandler } from "@bitbaum/mail-kit";
export const POST = inboundHandler({ onMail: (mail) => saveAndTriage(mail) });

Unsigned, tampered, stale (> 5 min) or oversized deliveries are refused before your code runs; a thrown onMail answers 500 so the gateway retries.

AI triage

triageMail(mail, complete, context) asks a model (any complete() — the ai-forms / ai-kit shape) for kind, urgency, a one-line summary, the sender's language and needsHuman. It never throws: no model, or nonsense back, and the answer comes from plain rules, marked by: "rules". Instructions inside an email are described, never followed.

Env contract (the whole of it)

VarMeaning
`RESEND_API_KEY`required to send (a fleet key: `/opt/fleet/env`)
`RESEND_FROM`sender — `Name <addr>` or bare address (optional if every call passes `from`)
`MAIL_ADDRESS`the app's own address; optional, derived from the app's URL
`MAIL_INBOUND_SECRET`verifies mail from the gateway (a fleet key: `/opt/fleet/env`)

Fleet conventions

  • The shared Resend account is on the free tier: 1 verified domain,

100 emails/day, 3,000/month. Hitting the daily cap surfaces as { sent: false, status: 429, retryable: true }.

  • Only loki.orangecat.ch is verified, so every app sends as

<app>@fleetcrown.orangecat.ch — conventionalFrom("My App") builds it.

  • A daily canary on the box sends one real email and alerts Telegram when it

fails — that, not this package, is what proves delivery keeps working.

What this is not

  • Not a template engine — apps own their HTML.
  • Not a queue — retryable tells a caller whether a retry can help;

scheduling one is the caller's decision.

  • Not for bulk — newsletters go through Listmonk.

Part of bitbaum — AI-native products on open infrastructure, built in Zürich. Every package here lists the apps that use it: which apps use mail-kit.

Changelog and releases

Read the repository changelog

This retrospective record was added on 2026-10-27 from the immutable sources linked below. Git commit dates are source dates, not inferred npm publication dates. A GitHub release does not itself establish npm publication. Application adoption and deployment evidence are tracked separately.

0.2.0

Recorded release notes

What's Changed

* ci(auto-merge): hand the sweep a PAT so workflow PRs stop stalling by @catomean in https://github.com/bitbaum/mail-kit/pull/2 * ci(auto-merge): correct the reason this repo passes a PAT by @catomean in https://github.com/bitbaum/mail-kit/pull/3 * ci(publish): make publishing idempotent so a second run is green, not E403 by @catomean in https://github.com/bitbaum/mail-kit/pull/4 * chore: rename FleetCrown to Loki by @catomean in https://github.com/bitbaum/mail-kit/pull/5 * fix: the sending domain is DNS, not the product name by @catomean in https://github.com/bitbaum/mail-kit/pull/6 * docs: link back to bitbaum, and to who uses this package by @catomean in https://github.com/bitbaum/mail-kit/pull/7 * feat: one address per app, mail to apps, and AI triage (v0.2.0) by @catomean in https://github.com/bitbaum/mail-kit/pull/8 * ci(publish): allow publishing a merged version by hand by @catomean in https://github.com/bitbaum/mail-kit/pull/9 * docs(readme): the one-address example names no product's door by @catomean in https://github.com/bitbaum/mail-kit/pull/10 * chore: add the fleet package template files this repo is missing (dependabot.yml, git-release.yml, release.yml) by @catomean in https://github.com/bitbaum/mail-kit/pull/11

Full Changelog: https://github.com/bitbaum/mail-kit/compare/v0.1.1...v0.2.0

  1. v0.2.0
    Release notes

    What's Changed

    * ci(auto-merge): hand the sweep a PAT so workflow PRs stop stalling by @catomean in https://github.com/bitbaum/mail-kit/pull/2 * ci(auto-merge): correct the reason this repo passes a PAT by @catomean in https://github.com/bitbaum/mail-kit/pull/3 * ci(publish): make publishing idempotent so a second run is green, not E403 by @catomean in https://github.com/bitbaum/mail-kit/pull/4 * chore: rename FleetCrown to Loki by @catomean in https://github.com/bitbaum/mail-kit/pull/5 * fix: the sending domain is DNS, not the product name by @catomean in https://github.com/bitbaum/mail-kit/pull/6 * docs: link back to bitbaum, and to who uses this package by @catomean in https://github.com/bitbaum/mail-kit/pull/7 * feat: one address per app, mail to apps, and AI triage (v0.2.0) by @catomean in https://github.com/bitbaum/mail-kit/pull/8 * ci(publish): allow publishing a merged version by hand by @catomean in https://github.com/bitbaum/mail-kit/pull/9 * docs(readme): the one-address example names no product's door by @catomean in https://github.com/bitbaum/mail-kit/pull/10 * chore: add the fleet package template files this repo is missing (dependabot.yml, git-release.yml, release.yml) by @catomean in https://github.com/bitbaum/mail-kit/pull/11

    Full Changelog: https://github.com/bitbaum/mail-kit/compare/v0.1.1...v0.2.0

Recent source changes

Repository activity can include unreleased work; it is not a list of deployed changes.

  1. docs(agents): the org rules block, generated from bitbaum/fleet (#25)
  2. chore(deps): clear dev-toolchain audit findings inside existing ranges (#24)
  3. docs(agents): the org rules block, generated from bitbaum/fleet (#23)
  4. fix(release): apply verified build-output staging contract (#22)
  5. chore(deps): dependabot leaves TypeScript 7 alone until typescript-eslint supports it (#20)
  6. docs(agents): the org rules block, generated from bitbaum/fleet (#19)
  7. docs: record verified release history and its evidence limits (#18)
  8. chore: add the fleet package template files this repo is missing (release-notify.yml) (#17)
All releases · All tags

Where it is declared

Linked products declare this package in their manifests. A declaration alone does not verify every feature, version or deployed integration.

Sources and verification

Install commands and declared consumers come from Fleet’s generated registry. Documentation and release notes come from the package repository. The catalogue does not certify production journey coverage.

Documentation from source commit d799d01a5233 · 2026-11-06. Source documentation can include work beyond the registry release.

Package verification runs