Skip to content

Examples

Construct one Carrier per Durable Object instance, using that object’s own storage and an Email Service binding. Forward the object’s alarm:

import { DurableObject } from "cloudflare:workers";
import type { SendEmail } from "@cloudflare/workers-types";
import type { Submission } from "@fungi.computer/carrier";
import { createCloudflareCarrier } from "@fungi.computer/carrier/cloudflare";
type Env = { EMAIL: SendEmail };
export class Mailbox extends DurableObject<Env> {
private readonly carrier;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.carrier = createCloudflareCarrier({
storage: ctx.storage,
email: env.EMAIL,
});
}
async send(email: Submission) {
return (await this.carrier).send(email);
}
async get(id: string) {
return (await this.carrier).get(id);
}
async alarm() {
await (await this.carrier).alarm();
}
}

Configure a SQLite Durable Object namespace and an Email Service binding in the host application. An application with a bound MAILBOX namespace can then submit and inspect mail:

const mailbox = env.MAILBOX.getByName("team-example");
const receipt = await mailbox.send({
id: "welcome:user-123:v1",
from: { email: "hello@example.com", name: "Example" },
to: ["alice@example.net", "bob@example.net"],
cc: ["host@example.net"],
bcc: ["archive@example.net"],
subject: "Welcome",
text: "Your account is ready.",
headers: { "List-Unsubscribe": "<https://example.com/unsubscribe/token>" },
});
const current = await mailbox.get(receipt.id);

send() needs at least one recipient across To, CC and BCC. To is optional. At least one of text or html is required. Display names and visible To and CC roles pass to the provider unchanged. BCC uses the provider’s native BCC field. Receipts expose all recipient addresses, BCC included, to the trusted caller, so do not forward whole receipts to recipients or other viewers. Optional provider headers are part of the immutable submission. A replay with different headers conflicts just like a replay with different content.

To use a different provider with Durable Object SQLite and alarms, pass transport instead of email. Exactly one of the two is required:

const carrier = await createCloudflareCarrier({
storage: ctx.storage,
transport: sendWithConfiguredProvider,
});
const inspection = createDeliveryInspection(ctx.storage.sql);
const emails = inspection.email({ limit: 32 });
const receipts = inspection.post({ view: "receipts", limit: 32 });
const dead = inspection.post({ view: "deadLetters", limit: 32 });

Unit tests exercise the native transport with controlled I/O, Post ordering, rollback, duplicate admission and recovery over local SQLite, and operator inspection. Cloudflare runtime tests exercise SQLite, alarms, eviction, interrupted dispatch and the D1 Post adapter without a live email binding. The PostgreSQL proof starts an isolated PostgreSQL 16 instance over a private Unix socket and always stops it. Install PostgreSQL 16, or set BOTANICAL_TEST_POSTGRES_ROOT to its extracted root. The D1 and PostgreSQL proofs cover producer, envelope and job rollback and commit, replay conflicts, restart, overlapping workers and fenced lease expiry. From the package directory:

Terminal window
pnpm test

No email is sent by these fixtures.