Samva is in early access — self-serve signup is limited. Have a team invite? Sign up with that email. Contact us for access.

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 samva

Create 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("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");

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("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");

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.

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.

On this page