Test and recover
Redirect recipients, use in-memory delivery, inspect failure history, cancel queued work, and retry terminal failures.
Redirect every recipient
Test mode changes the stored message before it is queued. It replaces to, removes cc and bcc, and adds metadata.convexEmailTestMode: true.
await email.setConfig(ctx, {
defaultFrom: "Acme <hello@acme.com>",
testMode: true,
sandboxTo: ["email-preview@acme.com"],
maxAttempts: 2,
retryBaseMs: 1_000,
cleanupAfterDays: 7,
});setConfig replaces the whole document, so include every value you want to keep. Test mode without sandboxTo fails before enqueueing and cannot contact the original recipients.
Use in-memory delivery
export const email = new ConvexEmail(components.convexEmail, {
adapters: [{ kind: "memory" }],
defaultAdapter: "memory",
});The memory adapter exercises the real queue, scheduler, status, event, idempotency, cancellation, and cleanup behavior without a network call.
For convex-test, register the packaged component helper:
import convexEmailTest from "@opencoredev/convex-email/test";
import { convexTest } from "convex-test";
export function createTest() {
const t = convexTest();
convexEmailTest.registerConvexEmail(t);
return t;
}Read the failure before retrying
const status = await email.status(ctx, { emailId });
const events = await email.listEvents(ctx, { emailId });Check lastError, attemptCount, attemptedAdapters, and the ordered events. Fix missing credentials, invalid sender configuration, unsupported message fields, or provider policy errors before starting another cycle.
Retry a terminal failure
import { v } from "convex/values";
import { internalMutation } from "./_generated/server";
import { email } from "./email";
export const retry = internalMutation({
args: { emailId: v.string() },
returns: v.boolean(),
handler: async (ctx, args) => email.retry(ctx, args),
});retry returns true only for a terminal failed record. It resets the attempt counter, clears the terminal error, appends a manual retry_scheduled event, and queues processing immediately. Sent and canceled messages are not retryable.
Use an idempotency key on the original message. Automatic recovery refuses to retry stale in-flight sends without one because the provider may already have accepted the message before the worker lost its result.
Expose only authorized operations
exposeApi() can generate app function definitions for send, batch, status, events, cancellation, and retry. It requires an authenticated Convex identity by default, records its subject as the email owner, and only returns or changes emails owned by that identity. Unauthenticated callers are rejected, and emails queued through server-only APIs remain private.
Use authorize when your app needs tenant ownership or an operation-specific policy. It receives the Convex auth context and operation name, and must return a stable owner id or null to deny the request. Configuration functions stay excluded unless both includeConfigApi: true and an explicit authorizeConfig admin check are provided.
Upgrade existing public APIs
exposeApi() now requires Convex authentication. Configure your app's Convex auth provider before upgrading callers that use it. Existing queued emails do not have a trusted public owner, so they remain private to server-side application wrappers instead of being backfilled with guessed ownership.
Most applications should write explicit wrappers instead. That keeps authentication, tenant checks, recipient policy, and administrator-only recovery visible in application code.
Send and track email
Understand queue state and event history.
Test core Email SDK
Use memory and failing adapters outside Convex.