npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pulgueta/usesend-convex

v0.3.0

Published

The Convex useSend Component - send emails durably with batching, rate limiting, and webhook support.

Readme

useSend Convex Component

npm version

This component is the official way to integrate the useSend email service with your Convex project. useSend is an open-source alternative to Resend, Sendgrid, Mailgun, and Postmark.

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 useSend'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 useSend idempotency keys to guarantee emails are delivered exactly once, preventing accidental spamming from retries.
  • Rate limiting: Honors API rate limits established by useSend.
  • Webhook support: Receive real-time email delivery status updates with a one-line usesend.registerRoutes(http) setup.
  • Self-hosted support: Works with both useSend's hosted service and self-hosted instances.

See example/convex/example.ts for a demo of how to incorporate this component into your application. Its public demo functions require Convex authentication and only allow the identity whose token identifier matches USESEND_EXAMPLE_ADMIN_TOKEN_IDENTIFIER.

Installation

npm install @pulgueta/usesend-convex

Get Started

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

Next, add the component to your Convex app via convex/convex.config.ts. Every environment variable the component can use is declared on the component and bound when installing it — the recommended setup binds them by reference to your deployment's env vars so the credential stays in deployment secret storage and is resolved at send time (it is never stored in component documents by current versions):

import { defineApp } from "convex/server";
import { v } from "convex/values";
import usesend from "@pulgueta/usesend-convex/convex.config.js";

const app = defineApp({
  env: {
    USESEND_API_KEY: v.string(),
    USESEND_BASE_URL: v.optional(v.string()),
  },
});
app.use(usesend, {
  env: {
    USESEND_API_KEY: app.env.USESEND_API_KEY,
    // optionals
    USESEND_BASE_URL: app.env.USESEND_BASE_URL,
  },
});

export default app;

If upgrading from <= 0.1.1, legacy email rows may still contain options.apiKey. After deploying, call components.usesend.lib.scrubApiKeys from an authenticated app mutation. Active legacy rows are failed while their keys are removed; re-enqueue those emails after the upgrade. Legacy work is never sent with a secret-bearing argument shape.

The component's env vars:

  • USESEND_API_KEY (required): the useSend API key used by the durable batch sender, resolved from deployment secret storage at send time.
  • USESEND_BASE_URL (optional): base URL for self-hosted useSend instances. When bound and set it takes precedence for durable batch sends; otherwise the per-instance baseUrl option (default https://app.usesend.com) is used.

USESEND_WEBHOOK_SECRET is intentionally not a component env var: webhook verification runs in your app's HTTP action (see below), so the secret is read app-side by the UseSend client.

Then you can use it in your Convex functions:

// convex/emails.ts
import { components } from "./_generated/api";
import { UseSend } from "@pulgueta/usesend-convex";
import { internalMutation } from "./_generated/server";

export const usesend = new UseSend(components.usesend);

export const sendTestEmail = internalMutation({
  handler: async (ctx) => {
    await usesend.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.

Advanced Usage

Setting up a useSend 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, register the component's routes on your HTTP router in convex/http.ts:

import { httpRouter } from "convex/server";
import { usesend } from "./emails";

const http = httpRouter();

usesend.registerRoutes(http);

export default http;

This mounts the webhook handler at /usesend/webhook. To mount it somewhere else, pass a custom endpoint:

usesend.registerRoutes(http, {
  endpoint: "/my/web/hook",
});

If your Convex project is happy-leopard-123, you now have a useSend webhook for your project running at https://happy-leopard-123.convex.site/usesend/webhook — or at whichever endpoint you passed above.

Navigate to the useSend dashboard and create a new webhook at that URL, matching the endpoint you registered. Make sure to enable all the email.* events.

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

Registering an email status event handler

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

import { components, internal } from "./_generated/api";
import { internalMutation } from "./_generated/server";
import { vOnEmailEventArgs, UseSend } from "@pulgueta/usesend-convex";

export const usesend = new UseSend(components.usesend, {
  onEmailEvent: internal.emails.handleEmailEvent,
});

export const handleEmailEvent = internalMutation({
  args: vOnEmailEventArgs,
  handler: async (ctx, args) => {
    console.log(`Email ${args.id} received event:`, args.event.type);

    switch (args.event.type) {
      case "email.delivered":
        console.log("Email delivered!");
        break;
      case "email.bounced":
        console.log("Email bounced");
        break;
      case "email.complained":
        console.log("Email marked as spam");
        break;
    }
  },
});

UseSend component options

There is a UseSendOptions argument to the component constructor to help customize its behavior:

  • baseUrl: The base URL for the useSend API (defaults to https://app.usesend.com). Set this if you're using a self-hosted useSend instance.
  • webhookSecret: Optional override for the useSend webhook secret. If omitted, it is read from the USESEND_WEBHOOK_SECRET environment variable.
  • initialBackoffMs: Initial backoff for retries (default: 30 seconds).
  • retryAttempts: Number of retry attempts (default: 5).
  • requestTimeoutMs: Maximum time to wait for a useSend API response (default: 30 seconds).
  • onEmailEvent: Your email event callback.

Using useSend Templates

You can use useSend templates to send emails with pre-designed templates from your useSend dashboard:

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

Note: You cannot use both template and html/text in the same email.

Scheduling and threading emails

Pass scheduledAt (ISO 8601) to have useSend deliver the email at a later time, or inReplyToId to thread it under a previously sent email:

await usesend.sendEmail(ctx, {
  from: "Me <[email protected]>",
  to: "[email protected]",
  subject: "See you tomorrow",
  html: "<p>Reminder!</p>",
  scheduledAt: "2026-08-01T09:00:00Z",
});

Emails already handed off to useSend with a future scheduledAt can be rescheduled or cancelled from the useSend dashboard or through the useSend REST API using the email's usesendId.

Tracking, getting status, and cancelling emails

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

  • Reassociating the original email during status changes in your email event handler.
  • Checking on the status any time using usesend.status(ctx, emailId).
  • Cancelling a waiting email using usesend.cancelEmail(ctx, emailId). Once batching starts, useSend may already be processing it and local cancellation is no longer safe.
// Check email status
const emailStatus = await usesend.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)
}

Self-hosted useSend

If you're running a self-hosted useSend instance, configure the baseUrl:

export const usesend = new UseSend(components.usesend, {
  baseUrl: "https://your-usesend-instance.com",
});

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 or set up a cron job:

// 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 usesend component",
  { hours: 1 },
  internal.crons.cleanupUseSend,
);

const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000;
export const cleanupUseSend = internalMutation({
  args: {},
  handler: async (ctx) => {
    await ctx.scheduler.runAfter(0, components.usesend.lib.cleanupOldEmails, {
      olderThan: ONE_WEEK_MS,
    });
    await ctx.scheduler.runAfter(
      0,
      components.usesend.lib.cleanupAbandonedEmails,
      { olderThan: 4 * ONE_WEEK_MS },
    );
  },
});

export default crons;

Using React Email

The component ships with a React Email integration at @pulgueta/usesend-convex/react-email. Author your emails as React components; sendReactEmail renders them to email-client-safe HTML plus a plain-text fallback (better accessibility and deliverability) and enqueues them through the durable send pipeline.

Install React Email in your app to author templates (the ./react-email module is an optional peer — it renders with your app's react-email install):

npm install react-email react-dom -E

Define a template (see react.email/docs for the client-compatibility rules — no flexbox/grid, pixel-based sizing, etc.):

// convex/emails/welcome.tsx
import {
  Body,
  Button,
  Container,
  Head,
  Html,
  Preview,
  Text,
} from "react-email";

export default function WelcomeEmail({ name }: { name: string }) {
  return (
    <Html lang="en">
      <Head />
      <Body style={{ fontFamily: "sans-serif" }}>
        <Preview>Welcome aboard!</Preview>
        <Container>
          <Text>{`Welcome, ${name}!`}</Text>
          <Button
            href="https://example.com"
            style={{ background: "#000", color: "#fff", padding: "12px 20px" }}
          >
            Get started
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

Then render and send it from an action (use a Node action for maximum compatibility with react-dom/server):

// convex/reactEmail.tsx
"use node";
import { internalAction } from "./_generated/server";
import { sendReactEmail } from "@pulgueta/usesend-convex/react-email";
import { v } from "convex/values";
import { usesend } from "./emails";
import WelcomeEmail from "./emails/welcome";

export const sendWelcomeEmail = internalAction({
  args: { to: v.string(), name: v.string() },
  returns: v.string(),
  handler: async (ctx, args) => {
    return await sendReactEmail(usesend, ctx, {
      from: "Onboarding <[email protected]>",
      to: args.to,
      subject: `Welcome, ${args.name}!`,
      react: <WelcomeEmail name={args.name} />,
    });
  },
});

In a plain .ts action (no JSX), call the component directly instead: react: WelcomeEmail({ name: args.name }). This also works for components typed as React.FC, as long as the component doesn't call hooks (a direct call runs outside React's renderer). And if you'd rather render yourself — e.g. with react-email's render — pass the output straight to usesend.sendEmail(ctx, { html, ... }).

If you only want the rendered output (e.g. to send it yourself through the useSend REST API with attachments), use renderEmail:

import { renderEmail } from "@pulgueta/usesend-convex/react-email";

const { html, text } = await renderEmail(<WelcomeEmail name="Ada" />);

See example/convex/emails/welcome.tsx for a fuller template using Tailwind with pixelBasedPreset.

Sending emails manually

If you want to bypass the component's batching (e.g. to attach files) while still tracking the email's delivery status through webhooks, use sendEmailManually. It records the email in the component, you perform the actual send in the callback (here by calling the useSend REST API directly), and the returned useSend ID links webhook events back to the record. The callback receives the component's record ID, which doubles as a stable Idempotency-Key so a retried send doesn't dispatch the email twice:

export const sendManualEmail = internalAction({
  args: {},
  returns: v.string(),
  handler: async (ctx) => {
    const from = "Acme <[email protected]>";
    const to = ["[email protected]"];
    const subject = "hello world";

    const emailId = await usesend.sendEmailManually(
      ctx,
      { from, to, subject },
      async (recordId) => {
        const response = await fetch("https://app.usesend.com/api/v1/emails", {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.USESEND_API_KEY}`,
            "Content-Type": "application/json",
            "Idempotency-Key": recordId,
          },
          body: JSON.stringify({
            from,
            to,
            subject,
            html: "<p>it works!</p>",
            attachments: [{ filename: "invoice.pdf", content: base64Pdf }],
          }),
        });
        if (!response.ok) {
          throw new Error(`useSend API error: ${response.status}`);
        }
        const result = await response.json();
        return result.emailId;
      },
    );
    return emailId;
  },
});

Development

To develop this component:

pnpm install
pnpm dev

This will start a file watcher to rebuild the component, as well as the example project frontend and backend.

License

Apache-2.0