SvelteKit
Send email with SvelteKit form actions and endpoints using the Samva SDK.
Send email with SvelteKit
Use the samva
SDK directly from SvelteKit server code. Form actions work well for in-app
forms, +server.ts endpoints work well for JSON clients, and both keep your API
key server-side.
Samva sends from the verified sender configured on your account, so the
payload has no from field.
Install
bun add samvaCreate a server-only client helper:
// src/lib/server/samva.ts
import { env } from "$env/dynamic/private";
import { createClient } from "samva";
export function getSamva() {
const apiKey = env.SAMVA_API_KEY;
if (!apiKey) {
throw new Error("SAMVA_API_KEY is not set.");
}
return createClient({ apiKey });
}$env/*/private and src/lib/server are server-only. Do not expose
SAMVA_API_KEY through a public env prefix.
Form action quickstart
Form actions let a SvelteKit page submit directly to server code:
// src/routes/contact/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";
import { getSamva } from "$lib/server/samva";
const escapeHtml = (value: string): string =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
const field = (formData: FormData, name: string): string =>
String(formData.get(name) ?? "").trim();
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
const email = field(formData, "email");
const message = field(formData, "message");
if (!email || !message) {
return fail(400, { error: "Email and message are required." });
}
await getSamva().messages.send({
to: [{ email }],
channel: "email",
email: {
subject: "Thanks for contacting us",
html: `<p>${escapeHtml(message).replaceAll("\n", "<br />")}</p>`,
text: message,
},
});
return { success: true };
},
} satisfies Actions;In +page.svelte, render the action result from the form prop and add
use:enhance
when you want pending state.
Endpoint quickstart
Use a +server.ts endpoint for raw HTTP:
// src/routes/api/send/+server.ts
import { error, json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { getSamva } from "$lib/server/samva";
const escapeHtml = (value: string): string =>
value
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
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() : "");
export const POST: RequestHandler = async ({ request }) => {
const body: unknown = await request.json().catch(() => null);
if (!isRecord(body)) {
error(400, "Expected a JSON object.");
}
const to = readString(body.to);
const subject = readString(body.subject);
const message = readString(body.message);
if (!to || !subject || !message) {
error(400, "to, subject, and message are required.");
}
await getSamva().messages.send({
to: [{ email: to }],
channel: "email",
email: {
subject,
html: `<p>${escapeHtml(message).replaceAll("\n", "<br />")}</p>`,
text: message,
},
});
return json({ ok: true });
};The endpoint awaits the send. Invalid payloads fail loudly with 400; missing
credentials throw from the server-only client module.
Cloudflare Workers
The SDK uses fetch, so the same send call works with
@sveltejs/adapter-cloudflare.
Prefer SvelteKit's $env/dynamic/private module for runtime secrets, or
$env/static/private when the key is available during build/typecheck. If the
key is only available as a Worker binding, construct the client per request from
platform.env:
import { error } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";
import { createClient } from "samva";
export const POST: RequestHandler = async ({ platform }) => {
const apiKey = platform?.env.SAMVA_API_KEY;
if (!apiKey) {
error(500, "SAMVA_API_KEY is not configured for this Worker.");
}
const samva = createClient({ apiKey });
// Read and validate request data, then call samva.messages.send().
};The runtime remains edge-safe as long as the code around the send avoids Node-only database drivers, filesystem access, SMTP sockets, and Node built-ins.
React Email
Render React Email to HTML, then send the strings with Samva:
import { render, toPlainText } from "react-email";
import WelcomeEmail from "$lib/emails/welcome";
import { getSamva } from "$lib/server/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 the full templating workflow.
Auth.js magic links
For @auth/sveltekit, replace the email provider's
sendVerificationRequest({ identifier, url }) body with a Samva send. Auth.js
still owns the verification token and requires a database adapter for email
sign-in.
async sendVerificationRequest({ identifier, url }) {
const { host } = new URL(url);
await getSamva().messages.send({
to: [{ email: identifier }],
channel: "email",
email: {
subject: `Sign in to ${host}`,
html: `<p><a href="${url}">Sign in to ${host}</a></p>`,
text: `Sign in to ${host}\n${url}\n`,
},
});
}Drop provider.from when porting from Resend or Nodemailer; Samva has no
from field.
Full cookbook and example
The cookbook and example links resolve after the companion
samva-integrations PR lands.