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/resend2. Scaffold User-Owned Files
npx kitcn add resendDefault scaffold set:
convex/lib/plugins/resend/schema.tsconvex/functions/plugins/resend.tsconvex/functions/plugins/email.tsxconvex/lib/plugins/resend/plugin.tsconvex/lib/plugins/resend/webhook.tsconvex/lib/plugins/resend/crons.ts
3. Register In Schema
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 codegenIf you scaffold email.tsx, install React Email deps:
bun add @react-email/components @react-email/render react-email react react-dom5. Configure Middleware Once
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.
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:
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
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
export { default } from '../lib/plugins/resend/crons';Runtime Surface
convex/functions/plugins/resend.ts contains the plugin internal procedures:
sendEmailcreateManualEmailupdateManualEmailcancelEmailgetStatusgethandleEmailEventcleanupOldEmailscleanupAbandonedEmails