← 返回博客

用 WorkOS AuthKit + Rovela 发事务邮件(2026)

用 Rovela POST /emails 发送自定义 WorkOS AuthKit 邀请、Magic Auth、验证与密码重置邮件——WorkOS 管身份/SSO,应用发送,Rovela 投递;不替代 WorkOS;与 Clerk、Cognito 指南区分。

更新于 2026年11月4日
用 WorkOS AuthKit + Rovela 发事务邮件(2026)

WorkOS 管身份 —— Rovela 只负责投递

WorkOS 是面向企业场景的身份平台:AuthKit(托管或自建登录 UI)、SSO、Directory Sync、组织、邀请、Magic Auth、邮箱验证与密码重置。AuthKit 默认可以代发这些邮件。许多 B2B 团队随后需要自定义事务邮件——品牌化邀请 HTML、双语 Magic Auth 验证码、独立声誉的 mail. 子域,或与账单/产品通知同一套投递栈。

本文保留 WorkOS 负责身份(AuthKit、SSO、Directory Sync、User Management),由可信后端处理 WorkOS webhook 或 API 响应,再通过 Rovela HTTP API 发送应用自有的 AuthKit 邮件:邀请、Magic Auth 验证码、邮箱验证、密码重置。

Rovela 不替代 WorkOS。 它不运行 SSO、Directory Sync、AuthKit 托管页、组织或会话签发。用户与组织仍以 WorkOS 为准;你的服务端拉取邀请 / Magic Auth / 验证对象(或从创建 API 的响应读取),Rovela 只负责入队、DKIM 签名与投递:POST https://api.rovela.dev/emails(发送邮件 API、快速开始)。

这不是 Clerk + Rovela,也不是 AWS Cognito 自定义邮件、Auth.js / NextAuth 或框架 OTP 教程。本文的集成面是 WorkOS AuthKit 自定义邮件(事件 + Get API)加上你的服务端密钥。

何时继续用 WorkOS 邮件,何时改走 Rovela

需求常见路径
AuthKit 默认的邀请 / Magic Auth / 验证 / 重置邮件先用 WorkOS 默认邮件即可
自定义 HTML、品牌版式、主题行带验证码、双语文案关闭默认邮件 → 应用发送 → Rovela
在 mail.yourdomain.com 上独立声誉已验证的 Rovela 域名 + 你的 from
非 AuthKit 模板的产品信(欢迎信、账单)一律应用自有 → Rovela
仍要 AuthKit、SSO 与 Directory Sync身份继续交给 WorkOS;只替换 AuthKit 邮件的传输层

常见做法:在 WorkOS Dashboard 关闭默认 AuthKit 邮件(Emails → Configuration → Manage),订阅相关事件(或从创建 API 响应直接发送),取回事件故意省略的密钥字段,再用 Rovela 投递。

你负责什么,Rovela 发什么

你的应用(加上 WorkOS)负责:

  • 用户、组织、SSO、Directory Sync 与 AuthKit(WorkOS)
  • 接管投递时关闭对应的默认 AuthKit 邮件
  • 处理 invitation.created / invitation.resent、magic_auth.created、email_verification.created、password_reset.created(webhook 或 Events API)
  • 调用 Get Invitation / Get Magic Auth / Get Email Verification / Get Password Reset,以获得 accept_invitation_url、code 或 password_reset_url(事件故意不含这些敏感字段)
  • 校验 WorkOS webhook 签名并去重
  • 每次逻辑发送使用稳定的 Idempotency-Key

Rovela 负责:

  • 使用 Authorization: Bearer 与 sending_access 密钥调用 POST /emails
  • 入队、签名、重试、退信/投诉处理

接口返回 200 表示已接受并入队,不等于「已进收件箱」(快速开始)。

前置条件

  1. 在 Rovela 控制台验证发信域名。未验证的 from 域名会以 400 拒绝(快速开始)。认证类邮件建议用 mail.example.com 这类子域(为何 OTP / 魔术链接进垃圾箱)。
  2. 形如 re_… 的 API 密钥。生产发信优先 sending_access(Authentication)。
  3. 已启用 AuthKit 的 WorkOS 环境与 API Key;若走事件驱动,还需 webhook。在 Emails → Configuration 关闭你将自行发送的邮件类型。
  4. 使用 AuthKit 托管流或自建接受页时,在 Applications → 你的应用 → Redirects 确认邀请与密码重置回调 URL。
  5. 切勿把 Rovela 密钥或 WorkOS 密钥放进浏览器或公开仓库。只有可信服务端可调用 Rovela 或 WorkOS API。

环境变量

# Node / Next.js 服务端示例 — 名称可按惯例调整
export ROVELA_API_KEY=re_your_sending_access_key
export ROVELA_FROM='Acme <security@mail.example.com>'
export WORKOS_API_KEY=sk_...                 # WorkOS API key
export WORKOS_WEBHOOK_SECRET=whsec_...       # WorkOS Dashboard → Webhooks

轮换时更新环境变量并重新部署;旧 Rovela 密钥在控制台撤销(Authentication)。WorkOS 与 Rovela 凭证放在主机已有的密钥存储中即可。

共用 Rovela 发送 helper

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

export async function sendRovelaEmail(opts: {
  to: string;
  subject: string;
  html: string;
  text: string;
  idempotencyKey: string;
}) {
  const apiKey = process.env.ROVELA_API_KEY!;
  const from = process.env.ROVELA_FROM!;

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

  const data = await res.json().catch(() => ({}));
  if (!res.ok) {
    // 不要把 API 密钥打进日志,只记录 status 与提供商 message
    const err = new Error(
      `rovela_send_failed ${res.status} ${data.message || ""}`,
    );
    (err as Error & { status?: number }).status = res.status;
    throw err;
  }
  return data as { object: string; id: string };
}

function escapeHtml(s: string) {
  return s
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

模式 A:邀请邮件(invitation.created / invitation.resent)

创建邀请时 WorkOS 发出 invitation.created,重发时发出 invitation.resent。事件载荷故意省略 token 与 accept_invitation_url。校验 webhook 签名后,用邀请 id 调用 Get Invitation,再向收件人发信。

请同时订阅 invitation.created 与 invitation.resent。只处理 created 时,控制台重发会静默不投递。

import { WorkOS } from "@workos-inc/node";

const workos = new WorkOS(process.env.WORKOS_API_KEY!);

// 在已验签、已去重的 webhook 处理函数内:
async function handleInvitationEvent(invitationId: string, eventId: string) {
  const invitation = await workos.userManagement.getInvitation(invitationId);
  // invitation.email、acceptInvitationUrl、expiresAt、organizationId、inviterUserId …

  const url = invitation.acceptInvitationUrl; // 或用 invitation.token 自建 URL
  const email = invitation.email;

  await sendRovelaEmail({
    to: email,
    subject: "邀请你加入 Acme",
    html:
      `<p>你被邀请加入 Acme。</p>` +
      `<p><a href="${escapeHtml(url)}">接受邀请</a></p>` +
      `<p>若按钮无效,请将以下链接粘贴到浏览器:</p>` +
      `<p>${escapeHtml(url)}</p>`,
    text:
      `你被邀请加入 Acme。\n\n` +
      `接受邀请:${url}\n`,
    idempotencyKey: `workos-invite-${invitationId}-${eventId}`,
  });
}

若后端通过 Send Invitation API 创建邀请,响应里已含密钥字段——可直接发送,同时保留 webhook 以覆盖在 Dashboard 手工创建的邀请。

多数 AuthKit 场景直接使用 accept_invitation_url。若自建接受页,用 token / invitation_token 拼 URL,并按 WorkOS 文档传入相应 authenticate 方法。与 AuthKit 无关的产品邀请仍由你的应用 + Rovela 发送(同样的 POST /emails)。

模式 B:Magic Auth 验证码(magic_auth.created)

Magic Auth 是 WorkOS 的邮箱验证码登录:创建或接收 Magic Auth 对象,把六位 code(按 WorkOS 文档在分钟级过期)发到邮箱,再由 AuthKit 或自建 UI 收集。事件不含 code;Get Magic Auth 会返回。

async function handleMagicAuthCreated(magicAuthId: string, eventId: string) {
  const magic = await workos.userManagement.getMagicAuth(magicAuthId);
  const email = magic.email;
  const code = magic.code; // 六位数字 — 生产环境不要打日志

  await sendRovelaEmail({
    to: email,
    subject: `你的 Acme 验证码:${code}`,
    html:
      `<p>你的登录验证码是</p>` +
      `<p style="font-size:28px;font-weight:600;letter-spacing:4px"><b>${escapeHtml(code)}</b></p>` +
      `<p>即将过期。如非本人操作,请忽略本邮件。</p>`,
    text:
      `你的登录验证码是 ${code}。即将过期。` +
      `如非本人操作,请忽略本邮件。`,
    idempotencyKey: `workos-magic-${magicAuthId}-${eventId}`,
  });
}

若你自行调用 Create Magic Auth,可从 API 响应直接发送,该次请求可跳过事件路径。建议把验证码同时放进主题行与正文,方便手机通知预览(邮箱验证码示例)。

模式 C:邮箱验证 + 密码重置

关闭默认邮件后,另外两类 AuthKit 邮件共用同一形状:

  1. 邮箱验证 — 处理 email_verification.created(或认证 API 返回的 email_verification_required 错误中的 id),Get Email Verification,把 code 发到 email。
  2. 密码重置 — 处理 password_reset.created,Get Password Reset,发送 password_reset_url(或用 password_reset_token 自建 URL)。确认应用 Redirects 中的密码重置回调。
async function handlePasswordResetCreated(resetId: string, eventId: string) {
  const reset = await workos.userManagement.getPasswordReset(resetId);
  const url = reset.passwordResetUrl;
  const email = reset.email;

  await sendRovelaEmail({
    to: email,
    subject: "重置你的 Acme 密码",
    html:
      `<p>请重置密码:</p>` +
      `<p><a href="${escapeHtml(url)}">选择新密码</a></p>` +
      `<p>如非本人请求,可忽略本邮件。</p>`,
    text:
      `重置密码:${url}\n` +
      `如非本人请求,请忽略本邮件。`,
    idempotencyKey: `workos-reset-${resetId}-${eventId}`,
  });
}

WorkOS 将四类 AuthKit 自定义邮件(邀请、Magic Auth、验证、密码重置)放在同一文档体系中说明。拿到链接或验证码之后,Rovela 只做传输——不要臆造 WorkOS 未以该方式提供的 SSO / Directory Sync「邮件功能」。

Webhook、Events API 与幂等

WorkOS 可通过 webhook 或 Events API 投递事件。无论哪种:

  • 信任载荷前先验签
  • 按至少一次语义处理:用事件 id 去重
  • 记住事件不含密钥——发送前务必 Get*
  • 传入稳定的 Rovela Idempotency-Key(≤ 256 字符,保留 24 小时),避免超时重试发出两封不一致的验证码或邀请(emails API)
  • 需要产生新邮件的故意重发必须使用新 key(WorkOS 的 invitation.resent 已提供新的 event id 可拼入)

仅在发送已被接受、或已安全写入你自己的任务队列后,再对 webhook 返回 2xx,以便 WorkOS 重试行为可预期。

你会真实碰到的错误

Rovela 错误形状统一(快速开始):

  • 400 并点名域名 — 本组织未验证该 from 域名
  • 400 recipient is suppressed: … — 曾退信/投诉;请换地址
  • 401 — 密钥无效或已撤销
  • 429 — 遵守 Retry-After;重试时保持同一 Idempotency-Key

API 已拒绝发送时,不要对用户说「请查收件箱」。在 webhook 里明确决定:Rovela 失败是返回非 2xx(触发 WorkOS 重试),还是先入本地队列再返回 200。

清单

  1. 在 Rovela 验证 mail.example.com(或你选择的域名);SPF/DKIM/DMARC 通过
  2. 创建 sending_access 密钥;仅存服务端环境变量,与 WORKOS_API_KEY 同级
  3. 在 WorkOS:关闭将自行发送的 AuthKit 邮件类型;注册 webhook(或轮询 Events);验签;订阅 created 以及 invitation.resent
  4. 发送前 Get Invitation / Magic Auth / Email Verification / Password Reset——不要仅凭稀疏事件拿密钥
  5. fetch 同时带 html 与 text,并加 Idempotency-Key
  6. AuthKit / SSO / Directory Sync 留在 WorkOS;Rovela 只做邮件传输
  7. 用控制台消息页或 Rovela webhook 观察投递——不要把 200 当成已进箱

相关文档:发送邮件 · Authentication · 邮箱验证码 · 快速开始 · WorkOS Custom Emails。