TanStack Start
Send email with TanStack Start server functions and server routes using the Samva SDK.
TanStack Start
Send transactional email from TanStack Start with the samva SDK. Use a server
function for in-app flows, or a server route when an external system needs to
POST raw HTTP to your app.
The same integration shape works on edge runtimes: server-only API keys, a fetch-based SDK, and React Email rendering when you need component templates.
Install
bun add samva zodSet a server-only API key. Do not use a VITE_ prefix.
SAMVA_API_KEY=sk_sm_your_key_here// src/lib/samva.ts
import "@tanstack/react-start/server-only";
import { createClient } from "samva";
export function getSamva() {
const apiKey = process.env.SAMVA_API_KEY;
if (!apiKey) {
throw new Error("SAMVA_API_KEY is not set");
}
return createClient({ apiKey });
}Build the client inside the handler. On Cloudflare Workers and other edge SSR
runtimes, process.env is injected per request, so module-scope env reads can
run too early.
Server function quickstart
// src/functions/send-email.ts
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import { getSamva } from "~/lib/samva";
const escapeHtml = (value: string) =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
export const sendEmail = createServerFn({ method: "POST" })
.inputValidator(
z.object({
to: z.string().email(),
subject: z.string().min(1),
message: z.string().min(1),
}),
)
.handler(async ({ data }) => {
const html = `<p>${escapeHtml(data.message).replaceAll("\n", "<br />")}</p>`;
await getSamva().messages.send({
to: [{ email: data.to }],
channel: "email",
email: {
subject: data.subject,
html,
text: data.message,
},
});
return { ok: true };
});Call it from a component with useServerFn:
import { useServerFn } from "@tanstack/react-start";
import { useState } from "react";
import { sendEmail } from "~/functions/send-email";
export function ContactForm() {
const send = useServerFn(sendEmail);
const [pending, setPending] = useState(false);
return (
<form
onSubmit={async (event) => {
event.preventDefault();
const form = new FormData(event.currentTarget);
setPending(true);
try {
await send({
data: {
to: String(form.get("to") ?? ""),
subject: String(form.get("subject") ?? ""),
message: String(form.get("message") ?? ""),
},
});
} finally {
setPending(false);
}
}}
>
<input name="to" type="email" required />
<input name="subject" required />
<textarea name="message" required />
<button disabled={pending}>{pending ? "Sending..." : "Send"}</button>
</form>
);
}There is no from field. Samva sends from the verified sender configured on
your account.
Server route quickstart
Use a server route for raw HTTP:
// src/routes/api/send.ts
import { createFileRoute } from "@tanstack/react-router";
import { z } from "zod";
import { getSamva } from "~/lib/samva";
const sendRouteInput = z
.object({
to: z.string().email(),
subject: z.string().min(1),
html: z.string().min(1).optional(),
text: z.string().min(1).optional(),
})
.refine((value) => value.html || value.text, {
message: "html or text is required",
});
const escapeHtml = (value: string) =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
export const Route = createFileRoute("/api/send")({
server: {
handlers: {
POST: async ({ request }) => {
const payload = await request.json().catch(() => null);
const parsed = sendRouteInput.safeParse(payload);
if (!parsed.success) {
return Response.json({ error: "Invalid send payload" }, { status: 400 });
}
const body = parsed.data;
const text = body.text;
const html = body.html ?? (text ? `<p>${escapeHtml(text).replaceAll("\n", "<br />")}</p>` : undefined);
await getSamva().messages.send({
to: [{ email: body.to }],
channel: "email",
email: {
subject: body.subject,
...(html ? { html } : {}),
...(text ? { text } : {}),
},
});
return Response.json({ ok: true });
},
},
},
});Server functions are RPC endpoints too, so enforce auth and rate limits inside the server function, middleware, or server route handler.
React Email
Add React Email when you want component templates:
bun add react-email react react-domimport { render, toPlainText } from "react-email";
import WelcomeEmail from "~/emails/welcome";
import { getSamva } from "~/lib/samva";
const html = await render(<WelcomeEmail name="Ada" />);
await getSamva().messages.send({
to: [{ email: "ada@example.com" }],
channel: "email",
email: {
subject: "Welcome",
html,
text: toPlainText(html),
},
});See the React Email integration for template structure, previewing, and edge rendering details.
Full cookbook and example
The cookbook and example links resolve after the companion
samva-integrations PR lands.