← Back to blog

Auth.js / NextAuth Email Provider with Rovela (2026)

Custom Auth.js sendVerificationRequest calling Rovela POST /emails with Idempotency-Key for magic-link sign-in—Auth.js owns tokens; Rovela delivers only. Distinct from Next.js magic-link, Clerk, and Supabase guides.

Updated November 4, 2026
Auth.js / NextAuth Email Provider with Rovela (2026)

Auth.js owns the magic link — Rovela only delivers it

Auth.js (the library formerly known as NextAuth.js) can sign users in with an Email provider: the user enters an address, Auth.js mints a verification token, stores a hashed copy via a database adapter, and emails a one-time URL. Built-in HTTP providers exist for a few vendors; for Rovela you wire a custom sendVerificationRequest (the same pattern Auth.js documents for “your own HTTP email”).

This guide shows that hook calling Rovela’s flat POST https://api.rovela.dev/emails with an Idempotency-Key, so Auth.js / NextAuth keep owning sessions and tokens while Rovela queues, DKIM-signs, and delivers (Send Email API, Quick Start, Authentication).

Rovela does not replace Auth.js. It does not create users, issue sessions, hash verification tokens, or decide whether a clicked link is valid. Auth.js (plus your adapter) owns the token lifecycle; your sendVerificationRequest only transports the already-built url to the inbox.

Distinct from:

What Auth.js does vs what Rovela sends

ConcernOwner
Email provider config, maxAge, adapter tablesAuth.js + your DB
Generating / hashing the verification tokenAuth.js
Building the callback url the user clicksAuth.js
HTML/text of the sign-in emailYour sendVerificationRequest
POST /emails, DKIM, queue, bouncesRovela
Redeeming the link and creating the sessionAuth.js

A 200 from Rovela means accepted and queued, not “already in the inbox” (Quick Start).

Prerequisites

  1. Verified sending domain in the Rovela dashboard. Unverified from domains are refused with 400 (Quick Start). Prefer something like mail.example.com for auth mail (Why OTP and magic-link emails go to spam).
  2. An API key shaped like re_…. Prefer sending_access for production senders (Authentication).
  3. Auth.js (or NextAuth.js v4) with a database adapter. Email sign-in requires an adapter; it is not optional for this provider type (Auth.js Email, Configuring your own HTTP Email).
  4. Keep ROVELA_API_KEY, AUTH_SECRET / NEXTAUTH_SECRET, and DB credentials on the server only—never in a client bundle.

Environment

export ROVELA_API_KEY=re_your_sending_access_key
export ROVELA_FROM='Acme <signin@mail.example.com>'
export AUTH_SECRET="$(openssl rand -hex 32)"   # Auth.js / NextAuth
# Database URL for your adapter (Postgres, etc.) — required for Email provider

Rotate Rovela keys by creating a new one, deploying, then revoking the old key (Authentication).

Register a custom Email provider

Auth.js accepts a provider object with type: "email" and your own sendVerificationRequest. The official HTTP-email guide uses a custom id such as http-email; you then call signIn("http-email", { email }) (or the equivalent in your framework) (Configuring your own HTTP Email).

Minimal Auth.js / Next.js sketch (auth.ts)—adapter omitted for brevity, but it must be configured:

import NextAuth from "next-auth";
import { sendVerificationRequest } from "./lib/rovelaSendVerificationRequest";
// import your database adapter and pass it as `adapter`

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter, // required for Email provider
  providers: [
    {
      id: "http-email",
      name: "Email",
      type: "email",
      maxAge: 60 * 60 * 24, // link validity — Auth.js default pattern is 24h
      sendVerificationRequest,
    },
  ],
  // pages / callbacks as needed for your app
});

NextAuth.js v4 users typically wrap the same idea with EmailProvider({ sendVerificationRequest, ... }) from next-auth/providers/email—the callback parameters (identifier, url, expires, provider, token, theme) are the same family of inputs (NextAuth Email provider).

sendVerificationRequest → Rovela POST /emails

Auth.js passes you the recipient (identifier) and the ready-to-click url. Do not ask Rovela to invent tokens. Escape hostnames carefully in HTML if you customize the template; never put the raw API key in logs.

// lib/rovelaSendVerificationRequest.ts
const ROVELA_URL = "https://api.rovela.dev/emails";

export async function sendVerificationRequest(params: {
  identifier: string;
  url: string;
  expires: Date;
  token: string;
  // provider, theme, request also available — see Auth.js Email types
}) {
  const { identifier: to, url, expires, token } = params;
  const host = new URL(url).host;
  const apiKey = process.env.ROVELA_API_KEY!;
  const from = process.env.ROVELA_FROM!;

  // Stable across transport retries for the same Auth.js token
  const idempotencyKey = `authjs-email-${token}`.slice(0, 256);

  const escapedHost = host.replace(/\./g, "&#8203;.");
  const html =
    `<p>Sign in to <strong>${escapedHost}</strong></p>` +
    `<p><a href="${url}">Sign in</a></p>` +
    `<p>This link expires at ${expires.toISOString()}.</p>` +
    `<p>If you did not request this email, you can ignore it.</p>`;
  const text =
    `Sign in to ${host}\n${url}\n\n` +
    `Expires: ${expires.toISOString()}\n`;

  const res = await fetch(ROVELA_URL, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      Accept: "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({
      from,
      to,
      subject: `Sign in to ${host}`,
      html,
      text,
    }),
  });

  if (!res.ok) {
    const data = await res.json().catch(() => ({}));
    // Do not log API keys — status + provider message only
    console.error("rovela_send_failed", res.status, data.message);
    throw new Error(`Rovela send failed: ${res.status}`);
  }
}

Notes aligned with Rovela’s docs:

  • Send both html and text when you can (Send Email API).
  • Idempotency-Key (≤ 256 chars): a retry within 24 hours returns the original acceptance instead of mailing twice—useful if your host retries the Auth.js request after a timeout (Send Email API — Idempotency). Prefer a key derived from Auth.js’s token (or another stable per-challenge id), not a random UUID on every attempt.
  • from must be on a verified domain or the API responds 400.

Throwing from sendVerificationRequest surfaces a failed sign-in email to Auth.js; that is intentional—silent success with no delivery is worse.

Triggering email sign-in

With provider id: "http-email":

import { signIn } from "@/auth"; // or next-auth/react client helper

await signIn("http-email", { email: "user@company.com" });

Auth.js generates the token, persists the hash through the adapter, builds url, then calls your sender. After the user clicks, Auth.js validates the token against the adapter and establishes the session—Rovela is not in that path.

Deliverability and ops checklist

  1. Authenticate SPF/DKIM/DMARC for the subdomain you use in ROVELA_FROM (DKIM setup, DMARC for SaaS).
  2. Keep auth mail on a dedicated subdomain so product newsletters cannot burn the same reputation (spam pitfalls).
  3. Prefer sending_access keys in production senders; use full_access only when you need to read message events (Authentication).
  4. Rate-limit how often a single address can request a new link at the application layer—Auth.js maxAge limits link lifetime, not request flooding.
  5. Do not put the verification token query string into Rovela tags or logs if those surfaces are broadly readable; the email body already carries the one-time URL.

Troubleshooting

SymptomLikely cause
Rovela 400 on sendfrom domain not verified, or missing html/text/template
Rovela 401Missing/invalid Bearer key (Authentication)
Auth.js rejects Email provider at bootNo database adapter configured
User never gets mail, send returns 200Queued but delayed/filtered—check domain auth and spam folder; inspect events with a full_access key if needed
Duplicate emails on retryMissing or non-stable Idempotency-Key

Summary

Wire Auth.js / NextAuth’s Email provider with a custom sendVerificationRequest that fetches Rovela POST /emails, set a stable Idempotency-Key, and keep secrets on the server. Auth.js remains the source of truth for tokens and sessions; Rovela is the transactional delivery layer—nothing more, nothing less.

Next steps: Quick Start · Send Email API · Auth.js HTTP email guide