Convex Autosend
Convex component for transactional email delivery on AutoSend—queueing, retries, idempotency, webhook verification, and delivery lifecycle tracking.
A Convex component for transactional email on top of AutoSend. Queue-first sending, deterministic idempotency, retry handling, and full delivery lifecycle.
#Features
- Queue-first sending —
sendEmailandsendBulkenqueue jobs; triggerprocessQueuefrom action/cron to send - Deterministic idempotency — Duplicate requests resolve to the same
emailId - Retry handling — Network, 429, 5xx retried with configurable backoff (default: 5s, 10s, 20s)
- Delivery lifecycle —
queued→sending→sent/failed/canceled - CC/BCC, attachments, templates — Full sender/recipient options, unsubscribe groups
- Webhook security — HMAC SHA-256 validation, timestamp skew limit, dedupe by
deliveryId - Batch status & events —
statusBatch,listEvents, maintenance cleanup (dry-run supported)
#Demo
convex-autosend.vercel.app — live demo of the component.
#Installation
npm install @mzedstudio/autosend convex#Setup
#1. Register the component
ts
// convex/convex.config.ts
import { defineApp } from "convex/server";
import autosend from "@mzedstudio/autosend/convex.config.js";
const app = defineApp();
app.use(autosend, { name: "autosend" });
export default app;#2. Create a client wrapper
ts
// convex/email.ts
import { AutoSend } from "@mzedstudio/autosend";
import { components } from "./_generated/api";
export const autosend = new AutoSend(components.autosend);#3. Configure secrets and runtime
bash
npx convex env set AUTOSEND_API_KEY <api-key>
npx convex env set AUTOSEND_WEBHOOK_SECRET <webhook-secret>Then persist config (e.g. via a one-time mutation):
ts
await autosend.setConfig(ctx, {
config: {
autosendApiKey: "replace-with-your-key",
webhookSecret: "replace-with-your-webhook-secret",
defaultFrom: "noreply@example.com",
testMode: true,
sandboxTo: ["sandbox@example.com"],
},
});#4. Mount webhook route
ts
// convex/http.ts
import { httpRouter } from "convex/server";
import { registerRoutes } from "@mzedstudio/autosend";
import { components } from "./_generated/api";
const http = httpRouter();
registerRoutes(http, components.autosend);
export default http;Default path: /webhooks/autosend.
#Usage
#Send and process queue
sendEmail / sendBulk enqueue only. Trigger processQueue from an action or cron:
ts
export const sendWelcome = mutation({
handler: async (ctx) => {
return await autosend.sendEmail(ctx, {
to: ["user@example.com"],
toName: "Jane Doe",
subject: "Welcome",
html: "<p>Hello</p>",
});
},
});
export const processEmailQueue = action({
handler: async (ctx) => {
return await autosend.processQueue(ctx);
},
});#Bulk send
ts
await autosend.sendBulk(ctx, {
recipients: ["a@example.com", "b@example.com"],
subject: "Update",
html: "<p>News</p>",
});#CC, BCC, attachments
ts
await autosend.sendEmail(ctx, {
to: ["user@example.com"],
cc: [{ email: "team@example.com", name: "Team" }],
subject: "Report",
html: "<p>See attached.</p>",
attachments: [
{ filename: "report.pdf", fileUrl: "https://example.com/report.pdf" },
{ filename: "data.csv", content: "base64...", contentType: "text/csv" },
],
unsubscribeGroupId: "marketing",
});#Templates
ts
await autosend.sendEmail(ctx, {
to: ["user@example.com"],
templateId: "welcome-template-id",
dynamicData: { firstName: "Jane", plan: "Pro" },
});#Status, batch status, events
ts
const email = await autosend.status(ctx, { emailId });
const statuses = await autosend.statusBatch(ctx, {
emailIds: [id1, id2, id3],
});
const events = await autosend.listEvents(ctx, { emailId, limit: 20 });
await autosend.cancelEmail(ctx, { emailId });#Cleanup
ts
const preview = await autosend.cleanupOldEmails(ctx, { dryRun: true });
await autosend.cleanupOldEmails(ctx, { olderThanMs: 7 * 24 * 60 * 60 * 1000 });
await autosend.cleanupAbandonedEmails(ctx, { staleAfterMs: 15 * 60 * 1000 });
await autosend.cleanupOldDeliveries(ctx, { olderThanMs: 7 * 24 * 60 * 60 * 1000 });#API Summary
| Method | Context | Notes |
|---|---|---|
sendEmail | mutation | Enqueues one email |
sendBulk | mutation | Enqueues up to 100 recipients |
status / statusBatch | query | Read email state |
listEvents | query | Webhook events per email |
cancelEmail | mutation | Only from queued or retrying |
processQueue | action | Sends due queued/retrying emails |
cleanupOldEmails | action | Removes old terminal emails |
cleanupAbandonedEmails | action | Recovers stale sending jobs |
#Config highlights
| Field | Default | Description |
|---|---|---|
rateLimitRps | 2 | Max sends per queue run |
retryDelaysMs | [5000, 10000, 20000] | Retry schedule |
maxAttempts | 4 | Total attempts including first |
testMode | true | Rewrites recipients to sandboxTo |