@convex-dev/resend

中文文档
0.2.10发布于 2026/10/23Apache-2.0TypeScriptES Module近 30 天下载 84.2万

A resend component for Convex.

原始使用文档
文档目录13 节
  1. Installation
  2. Get Started
  3. Advanced Usage
  4. Setting up a Resend webhook
  5. Registering an email status event handler.
  6. Resend component options, and going into production
  7. Optional email sending parameters
  8. Enqueue-time idempotency
  9. Using Resend Templates
  10. Tracking, getting status, and cancelling emails
  11. Data retention
  12. Using React Email
  13. Sending emails manually, e.g. for attachments

Resend Convex Component

npm version

This component is the official way to integrate the Resend email service with your Convex project.

Features:

  • Queueing: Send as many emails as you want, as fast as you want—they'll all be delivered (eventually).
  • Batching: Automatically batches large groups of emails and sends them to Resend's /emails/batch endpoint efficiently.
  • Durable execution: Uses Convex workpools to ensure emails are eventually delivered, even in the face of temporary failures or network outages.
  • Idempotency: Manages Resend idempotency keys to guarantee emails are delivered exactly once, preventing accidental spamming from retries.
  • Rate limiting: Honors API rate limits established by Resend.

See example for a demo of how to incorporate this hook into your application.

Navigate the Email MINEFIELD with the Resend Component!

Installation

npm install @convex-dev/resend

Get Started

Create a Resend account and grab an API key. Set it to RESEND_API_KEY in your deployment environment.

Next, add the component to your Convex app via convex/convex.config.ts:

import { defineApp } from "convex/server";
import resend from "@convex-dev/resend/convex.config.js";

const app = defineApp();
app.use(resend);

export default app;

Then you can use it, as we see in convex/sendEmails.ts:

import { components } from "./_generated/api";
import { Resend } from "@convex-dev/resend";
import { internalMutation } from "./_generated/server";

export const resend: Resend = new Resend(components.resend, {});

export const sendTestEmail = internalMutation({
  handler: async (ctx) => {
    await resend.sendEmail(ctx, {
      from: "Me <[email protected]>",
      to: "[email protected]",
      subject: "Hi there",
      html: "This is a test email",
    });
  },
});

Then, calling sendTestEmail from anywhere in your app will send this test email.

If you want to send emails to real addresses, you need to disable testMode. You can do this in ResendOptions, as detailed below.

A note on test email addresses: Resend allows the use of labels for test emails. For simplicity, this component only allows labels matching [a-zA-Z0-9_-]*, e.g. [email protected].

Advanced Usage

Setting up a Resend webhook

While the setup we have so far will reliably send emails, you don't have any feedback on anything delivering, bouncing, or triggering spam complaints. For that, we need to set up a webhook!

On the Convex side, we need to mount an http endpoint to our project to route it to the Resend component in convex/http.ts:

import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
import { resend } from "./sendEmails";

const http = httpRouter();

http.route({
  path: "/resend-webhook",
  method: "POST",
  handler: httpAction(async (ctx, req) => {
    return await resend.handleResendEventWebhook(ctx, req);
  }),
});

export default http;

If our Convex project is happy-leopard-123, we now have a Resend webhook for our project running at https://happy-leopard-123.convex.site/resend-webhook.

So navigate to the Resend dashboard and create a new webhook at that URL. Make sure to enable all the email.* events; the other event types will be ignored.

Finally, copy the webhook secret out of the Resend dashboard and set it to the RESEND_WEBHOOK_SECRET environment variable in your Convex deployment.

You should now be seeing email status updates as Resend makes progress on your batches!

Speaking of...

Registering an email status event handler.

If you have your webhook established, you can also register an event handler in your apps you get notifications when email statuses change.

Update your sendEmails.ts to look something like this:

import { components, internal } from "./_generated/api";
import { internalMutation } from "./_generated/server";
import { vEmailId, vEmailEvent, Resend } from "@convex-dev/resend";

export const resend: Resend = new Resend(components.resend, {
  onEmailEvent: internal.example.handleEmailEvent,
});

export const handleEmailEvent = internalMutation({
  args: vOnEmailEventArgs,
  handler: async (ctx, args) => {
    // Handle however you want
    // args provides { id: EmailId; event: EmailEvent; }
    // see /example/example.ts
  },
});

Check out the example/ project in this repo for a full demo.

Resend component options, and going into production

There is a ResendOptions argument to the component constructor to help customize it's behavior.

Check out the docstrings, but notable options include:

  • apiKey: Provide the Resend API key instead of having it read from the environment variable.
  • webhookSecret: Same thing, but for the webhook secret.
  • testMode: Only allow delivery to test addresses. To keep you safe as you develop your project, testMode is default true. You need to explicitly set this to false for the component to allow you to enqueue emails to artibrary addresses.
  • onEmailEvent: Your email event callback, as outlined above! Check out the docstrings for details on the events that are emitted.

Optional email sending parameters

In addition to basic from/to/subject and html/plain text bodies, the sendEmail method allows you to provide a list of replyTo addresses, and other email headers.

Enqueue-time idempotency

The component already sends idempotency keys to Resend so a given batch is delivered at most once. That protects against the component retrying an API call, but it doesn't stop your application from enqueuing the same logical email twice (for example, when a business flow re-fires or an action is retried).

To dedupe at enqueue time, pass an idempotencyKey. If an email with the same key has already been enqueued, sendEmail returns the existing EmailId instead of enqueuing (and delivering) a duplicate:

const emailId = await resend.sendEmail(ctx, {
  from: "Me <[email protected]>",
  to: "[email protected]",
  subject: "Order confirmation",
  html: "...",
  // Derive this from the triggering event so retries collapse to one send.
  idempotencyKey: `order-confirmation:${orderId}`,
});

Because sendEmail runs in a transactional mutation, concurrent enqueues with the same key are safe: one wins and the others receive its EmailId.

Using Resend Templates

You can use Resend templates to send emails with pre-designed templates from your Resend dashboard. To use a template, provide the template ID and any required template variables:

await resend.sendEmail(ctx, {
  from: "Me <[email protected]>",
  to: "[email protected]",
  subject: "Welcome to our app",
  template: {
    id: "my-template-id",
    variables: {
      name: "John Doe",
      verificationLink: "https://example.com/verify?token=abc123",
    },
  },
});

[!IMPORTANT] You cannot use both template and html/text in the same email. If you need to send dynamic HTML content, either use templates with template variables, or use the html/text fields directly (optionally with React Email).

Tracking, getting status, and cancelling emails

The sendEmail method returns a branded type, EmailId. You can use this for a few things:

  • To reassociate the original email during status changes in your email event handler.
  • To check on the status any time using resend.status(ctx, emailId).
  • To cancel the email using resend.cancelEmail(ctx, emailId).

If the email has already been sent to the Resend API, it cannot be cancelled. Cancellations do not trigger an email event.

Checking email status programmatically

Use the status method to check an email's current state:

const emailStatus = await resend.status(ctx, emailId);
if (emailStatus) {
  console.log(emailStatus.status); // e.g., "delivered", "bounced", "sent"
  console.log(emailStatus.bounced); // boolean
  console.log(emailStatus.failed); // boolean
  console.log(emailStatus.complained); // spam complaint (boolean)
  console.log(emailStatus.deliveryDelayed); // boolean
  console.log(emailStatus.opened); // if open tracking enabled (boolean)
  console.log(emailStatus.clicked); // if click tracking enabled (boolean)
  console.log(emailStatus.errorMessage); // error details (string | null)
}

Viewing emails and webhook events in the dashboard

You can view all email data directly in your Convex dashboard in the component's data view. Click the drop down with a puzzle piece that says app:

Component tables screenshot

  1. Emails table: Navigate to your Convex dashboard → Data. Choose resend from the component drop down then choose the emails table. This shows all emails with their current status, recipients, subjects, and tracking information.

  2. Delivery Events table: Navigate to Components → resend → deliveryEvents table. This table stores all webhook events received from Resend, including:

    • emailId: Links back to the email in the emails table
    • resendId: Resend's ID for the email
    • eventType: The type of event (e.g., email.delivered, email.bounced, email.opened, email.clicked, email.complained)
    • createdAt: When the event occurred
    • message: Additional details (e.g., bounce reasons)

This is useful for debugging delivery issues, viewing email history, and understanding what happened with each email you sent.

Data retention

This component retains "finalized" (delivered, cancelled, bounced) emails. It's your responsibility to clear out those emails on your own schedule. You can run cleanupOldEmails and cleanupAbandonedEmails from the dashboard, under the "resend" component tab in the function runner, or set up a cron job.

If you pass no argument, it defaults to deleting emails older than 7 days.

If you don't care about historical email status, the recommended approach is to use a cron job, as shown below:

// in convex/crons.ts
import { cronJobs } from "convex/server";
import { components, internal } from "./_generated/api.js";
import { internalMutation } from "./_generated/server.js";

const crons = cronJobs();
crons.interval(
  "Remove old emails from the resend component",
  { hours: 1 },
  internal.crons.cleanupResend,
);

const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
export const cleanupResend = internalMutation({
  args: {},
  handler: async (ctx) => {
    await ctx.scheduler.runAfter(0, components.resend.lib.cleanupOldEmails, {
      olderThan: ONE_WEEK_MS,
    });
    await ctx.scheduler.runAfter(
      0,
      components.resend.lib.cleanupAbandonedEmails,
      // These generally indicate a bug, so keep them around for longer.
      { olderThan: 4 * ONE_WEEK_MS },
    );
  },
});

export default crons;

Using React Email

You can use React Email to generate your HTML for you from JSX.

First install the dependencies:

npm install @react-email/components react react-dom react-email @react-email/render

Then create a new .tsx file in your Convex directory e.g. /convex/emails.tsx:

// IMPORTANT: this is a Convex Node Action
"use node";
import { action } from "./_generated/server";
import { render, pretty } from "@react-email/render";
import { Button, Html } from "@react-email/components";
import { components } from "./_generated/api";
import { Resend } from "@convex-dev/resend";

export const resend: Resend = new Resend(components.resend, {
  testMode: false,
});

export const sendEmail = action({
  args: {},
  handler: async (ctx, args) => {
    // 1. Generate the HTML from your JSX
    // This can come from a custom component in your /emails/ directory
    // if you would like to view your templates locally. For more info see:
    // https://react.email/docs/getting-started/manual-setup#5-run-locally
    const html = await pretty(
      await render(
        <Html>
          <Button
            href="https://example.com"
            style={{ background: "#000", color: "#fff", padding: "12px 20px" }}
          >
            Click me
          </Button>
        </Html>,
      ),
    );

    // 2. Send your email as usual using the component
    await resend.sendEmail(ctx, {
      from: "Me <[email protected]>",
      to: "[email protected]",
      subject: "Hi there",
      html,
    });
  },
});

[!WARNING] React Email requires some Node dependencies thus it must run in a Convex Node action and not a regular Action.

Sending emails manually, e.g. for attachments

If you need something that the component doesn't provide (it is currently limited by what is supported by the batch API in Resend), you can send emails manually using sendEmailManually. Unlike sendEmail which enqueues emails and sends them in batches via the /emails/batch endpoint, sendEmailManually calls Resend's /emails endpoint directly without enqueueing. This gives you fine-grained control over the email sending process while still tracking its progress using the component's status and webhook APIs.

import { components, internal } from "./_generated/api";
import { internalAction } from "./_generated/server";
import { Resend as ResendComponent } from "@convex-dev/resend";
import { Resend } from "resend";

const resendSdk = new Resend("re_xxxxxxxxx");

export const resend = new ResendComponent(components.resend, {});

export const sendManualEmail = internalAction({
  args: {},
  handler: async (ctx, args) => {
    const from = "Acme <[email protected]>";
    const to = ["[email protected]"];
    const subject = "hello world";
    const html = "<p>it works!</p>";

    const emailId = await resend.sendEmailManually(
      ctx,
      { from, to, subject },
      async (emailId) => {
        const {data, error} = await resendSdk.emails.send({
          from,
          to,
          subject,
          html,
          headers: {
            "Idempotency-Key": emailId,
          },
        });
        if (error) {
          throw new Error(`[Email] Failed to send: ${error.message}`);
        }
        return data.id!;
      },
    );
  },
});

Use sendEmailManually when you need features not supported by the batch API, such as attachments, or when you want to send an email immediately without waiting for the batching system.

版本与发布记录

@convex-dev/resend 共发布 32 个版本,首次发布于 2025/06/14;过去 12 个月发布了 11 个版本。

版本发布日期距今
0.2.10latest2026/10/2318 天前
0.2.92026/10/101 个月前
0.2.82026/09/022 个月前
0.2.72026/08/222 个月前
0.2.62026/07/283 个月前
0.2.52026/06/174 个月前

其他发布通道:alpha → 0.1.6-alpha.1。

关于 @convex-dev/resend 的常见问题

@convex-dev/resend 是什么?

A resend component for Convex.

如何安装 @convex-dev/resend?

在项目目录运行 npm install @convex-dev/resend,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:convex(^1.43.0)、convex-helpers(^0.1.106)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@convex-dev/resend 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@convex-dev/resend 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@convex-dev/resend 可以商用吗?

@convex-dev/resend 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@convex-dev/resend 的下载量有多少?

根据 npm 官方下载统计,@convex-dev/resend 在 2026/10/11 – 2026/11/09 这 30 天内被下载 842,038 次,日均约 28,068 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@convex-dev/resend 还在维护吗?

最新版本 0.2.10 发布于 2026/10/23(18 天前);至今共发布 32 个版本,其中过去 12 个月发布了 11 个;首次发布于 2025/06/14。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。