BETTER-CONVEX

Resend

Durable queued email delivery for kitcn with batching, retries, webhook ingestion, and cleanup.

@kitcn/resend is the Resend plugin for kitcn.

What You Get

  • Durable queueing and batching to Resend /emails/batch
  • Retries with backoff for transient failures
  • Idempotency-key handling
  • Rate pacing for batch calls
  • Webhook ingestion and status tracking

1. Install

bun add @kitcn/resend

2. Scaffold User-Owned Files

npx kitcn add resend

Default scaffold set:

  • convex/lib/plugins/resend/schema.ts
  • convex/functions/plugins/resend.ts
  • convex/functions/plugins/email.tsx
  • convex/lib/plugins/resend/plugin.ts
  • convex/lib/plugins/resend/webhook.ts
  • convex/lib/plugins/resend/crons.ts

3. Register In Schema

convex/functions/schema.ts
import { defineSchema } from 'kitcn/orm';
import { resendExtension } from '../lib/plugins/resend/schema';

export default defineSchema(tables).extend(resendExtension());

kitcn add resend writes this for you. Customize resend tables and relations by editing convex/lib/plugins/resend/schema.ts directly.

If paths.env is missing, kitcn add resend also creates convex/lib/get-env.ts, writes paths.env into concave.json, and adds:

  • RESEND_API_KEY: z.string().optional()
  • RESEND_WEBHOOK_SECRET: z.string().optional()
  • RESEND_FROM_EMAIL: z.string().optional()

The add command also reminds you to set RESEND_API_KEY in convex/.env before sending real email.

4. Generate Framework Files

npx kitcn codegen

If you scaffold email.tsx, install React Email deps:

bun add @react-email/components @react-email/render react-email react react-dom

5. Configure Middleware Once

convex/lib/plugins/resend/plugin.ts
import { ResendPlugin } from '@kitcn/resend';
import { getEnv } from '../../get-env';

export const resend = ResendPlugin.configure({
  apiKey: getEnv().RESEND_API_KEY,
  webhookSecret: getEnv().RESEND_WEBHOOK_SECRET,
  initialBackoffMs: 30_000,
  retryAttempts: 5,
  testMode: true,
});

testMode defaults to true; only Resend test recipients are allowed until you set testMode: false.

6. Use In cRPC Procedures

Use middleware for config/context and call scaffolded internal functions through generated callers.

convex/functions/sendEmail.ts
import { createPluginsResendCaller } from './generated/plugins/resend.runtime';
import { privateMutation } from '../lib/crpc';
import { resend } from '../lib/plugins/resend/plugin';

export const sendTestEmail = privateMutation.use(resend.middleware())
  .mutation(async ({ ctx }) => {
    const caller = createPluginsResendCaller(ctx);
    await caller.sendEmail({
      from: 'Me <test@mydomain.com>',
      to: ['delivered@resend.dev'],
      subject: 'Hi there',
      html: 'This is a test email',
    });
  });

7. Generic React Email Action

convex/functions/plugins/email.tsx is a generic internal cRPC action scaffold:

  • Input: to, from?, subject, title, body, ctaLabel?, ctaUrl?
  • Sender resolution: from ?? getEnv().RESEND_FROM_EMAIL
  • Delivery: createPluginsResendCaller(ctx).sendEmail(...)

Use it from auth organization invites:

convex/functions/auth.ts
await actionCtx.scheduler.runAfter(
  0,
  internal.plugins.email.sendTemplatedEmail,
  {
    to: data.email,
    subject: `${inviterName} invited you to join ${organizationName}`,
    title: `Invitation to join ${organizationName}`,
    body: `${inviterName} (${data.inviter.user.email}) invited you to join ${organizationName}${roleSuffix}.`,
    ctaLabel: 'Accept invitation',
    ctaUrl: acceptUrl,
  }
);

8. Wire Webhook Route

convex/functions/http.ts
import { router } from '../lib/crpc';
import { resendWebhook } from '../lib/plugins/resend/webhook';
import { health } from '../routers/health';

export const httpRouter = router({
  health,
  resendWebhook,
});

9. Optional Event Fanout

convex/functions/plugins/resend.ts includes scaffolded onEmailEvent. Customize it directly.

10. Optional Cleanup Crons

convex/functions/crons.ts
export { default } from '../lib/plugins/resend/crons';

Runtime Surface

convex/functions/plugins/resend.ts contains the plugin internal procedures:

  • sendEmail
  • createManualEmail
  • updateManualEmail
  • cancelEmail
  • getStatus
  • get
  • handleEmailEvent
  • cleanupOldEmails
  • cleanupAbandonedEmails

On this page