Hono on Cloudflare Workers
Send email with Hono on Cloudflare Workers using Samva, Worker secrets, and an edge-native fetch send path.
Hono on Cloudflare Workers
Send email from a Hono app running on
Cloudflare Workers with the
samva SDK. The send path runs on Workers with fetch, so it does not need
SMTP, Node APIs, or nodejs_compat.
Use Worker bindings for secrets. Read c.env.SAMVA_API_KEY inside the route
handler instead of process.env or a module-top client.
Install
bun add hono samva
bun add -d wrangler @cloudflare/workers-types typescriptStore the Samva API key as a Worker secret:
wrangler secret put SAMVA_API_KEYFor local development, put the same binding in .dev.vars:
SAMVA_API_KEY="sk_sm_..."Route handler
import { Hono } from "hono";
import { createClient } from "samva";
type Bindings = {
SAMVA_API_KEY: string;
};
type SendRequestBody = {
to?: unknown;
subject?: unknown;
html?: unknown;
text?: unknown;
};
const app = new Hono<{ Bindings: Bindings }>();
const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const readString = (value: unknown): string => (typeof value === "string" ? value.trim() : "");
app.post("/send", async (c) => {
const body: unknown = await c.req.json().catch(() => null);
if (!isRecord(body)) {
return c.json({ error: "Expected a JSON object." }, 400);
}
const { to, subject, html, text } = body as SendRequestBody;
const recipientEmail = readString(to);
const emailSubject = readString(subject);
if (!recipientEmail || !emailSubject) {
return c.json({ error: "to and subject are required" }, 400);
}
const samva = createClient({ apiKey: c.env.SAMVA_API_KEY });
const { data, error } = await samva.messages.send({
to: [{ email: recipientEmail }],
channel: "email",
email: {
subject: emailSubject,
html: readString(html) || "<p>Hello from Hono on Cloudflare Workers.</p>",
text: readString(text) || undefined,
},
});
if (error) {
return c.json({ ok: false, error }, 502);
}
return c.json({ ok: true, id: data?.id });
});
export default app;Samva sends from the verified sender configured on your account, so the payload
has no from field.
Worker config
Use a module Worker entrypoint and omit Node compatibility flags:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "hono-cloudflare-workers-samva",
"main": "src/index.ts",
"compatibility_date": "2026-06-29"
}Hono's export default app maps to the Worker fetch(request, env, ctx) entry,
and Hono exposes Worker bindings through c.env.
waitUntil
Await samva.messages.send(...) for user-facing sends where the caller should
see API errors. Use c.executionCtx.waitUntil(...) only for background work
where returning immediately matters more than surfacing the send result.
c.executionCtx.waitUntil(
samva.messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: {
subject: "Background notification",
html: "<p>This send runs after the response.</p>",
},
}),
);React Email
React Email's render package has a Workers-compatible edge build. Render to HTML, derive text, then pass both strings to Samva:
bun add @react-email/render react react-domimport { render, toPlainText } from "@react-email/render";
const html = await render(<WelcomeEmail name="Ada" />);
await samva.messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: {
subject: "Welcome",
html,
text: toPlainText(html),
},
});For deeper templating, see the React Email integration.
Webhook receiver shape
If the Worker receives Samva webhooks, read the raw body before parsing JSON.
Signature verification belongs in the samva/webhooks SDK subpath.
Do not deploy this receiver until it verifies the Samva webhook signature. The stub below only shows the raw-body shape that verification needs.
app.post("/webhooks/samva", async (c) => {
const payload = await c.req.text();
const signature = c.req.header("x-webhook-signature");
// TODO(wave 3): verify payload and signature with samva/webhooks.
void payload;
void signature;
return c.body(null, 204);
});Cookbook and example
The cookbook and example links resolve after the companion
samva-integrations PR lands.
- Hono on Cloudflare Workers cookbook - complete setup, send route,
waitUntil, React Email, and webhook notes. hono-cloudflare-workersexample - runnable Worker withwrangler.jsonc,.dev.vars.example,POST /send, and webhook stub.- Hono Cloudflare Workers docs
- Cloudflare Workers docs