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

Auth.js

Send Auth.js and NextAuth magic-link email through Samva with a custom HTTP email provider.

Auth.js

Auth.js email providers send magic links through one callback: sendVerificationRequest. Use a custom HTTP email provider and call samva.messages.send from that callback.

Auth.js owns authentication and verification-token storage. Samva sends the email over your verified sender, so the Samva payload has no from field.

Install

bun add next-auth@beta samva

Keep SAMVA_API_KEY server-side only. Auth.js also needs AUTH_SECRET; generate it with:

npx auth secret

Add the provider

import NextAuth from "next-auth";
import type { EmailConfig } from "next-auth/providers/email";
import { createClient } from "samva";

const apiKey = process.env.SAMVA_API_KEY;
if (!apiKey) {
  throw new Error("SAMVA_API_KEY is not set.");
}

const samva = createClient({ apiKey });

function SamvaEmail(config: Partial<EmailConfig> = {}): EmailConfig {
  return {
    id: "samva",
    type: "email",
    name: "Email",
    from: "",
    maxAge: 24 * 60 * 60,
    async sendVerificationRequest({ identifier: to, url }) {
      const { host } = new URL(url);

      await samva.messages.send({
        to: [{ email: to }],
        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`,
        },
      });
    },
    options: config,
  };
}

export const { handlers, signIn, signOut, auth } = NextAuth({
  // adapter: <your database adapter>,
  providers: [SamvaEmail()],
});

The empty provider-level from exists only because Auth.js' email-provider type still carries that field. It is not forwarded to Samva.

Add the route handler

import { handlers } from "@/auth";

export const { GET, POST } = handlers;

Place that in app/api/auth/[...nextauth]/route.ts.

Trigger sign-in

import { signIn } from "@/auth";

export function SignInForm() {
  return (
    <form
      action={async (formData) => {
        "use server";
        await signIn("samva", formData);
      }}
    >
      <input type="email" name="email" required />
      <button type="submit">Email me a sign-in link</button>
    </form>
  );
}

Template with React Email

Render a React Email component to html, derive text, then pass both strings to Samva. Replace sendVerificationRequest inside the SamvaEmail provider factory with:

import { render, toPlainText } from "react-email";
import { MagicLinkEmail } from "./emails/magic-link";

async sendVerificationRequest({ identifier: to, url }) {
  const { host } = new URL(url);
  const html = await render(<MagicLinkEmail host={host} url={url} />);
  const text = toPlainText(html);

  await samva.messages.send({
    to: [{ email: to }],
    channel: "email",
    email: { subject: `Sign in to ${host}`, html, text },
  });
}

See the React Email integration for the full rendering and preview workflow.

Runtime notes

The Samva send is edge-safe because the SDK is fetch-based. The database adapter you choose for Auth.js verification-token storage is the runtime gate: many adapters need Node, or Auth.js' split-config pattern with auth.config.ts for edge proxy/middleware and full auth.ts with the adapter elsewhere.

NextAuth v4's EmailProvider uses the same sendVerificationRequest seam, but the wiring differs; see the Auth.js v5 migration guide.

For delivery and bounce events, see Verify webhooks.

Cookbook

The cookbook link resolves after the companion samva-integrations PR lands.

On this page