Cloudflare Email Sender

Send transactional email via Cloudflare's REST API from Convex mutations with queuing, deduplication, status tracking, and bounded retries.

Installation

npm install convex-cloudflare-email

About Cloudflare Email Sender

convex-cloudflare-email sends email through Cloudflare’s beta Email Sending REST API from Convex. It provides transactional enqueueing, send-status tracking, idempotency keys, cancellation, and bounded retries for HTTP 429 responses.

The component uses a workpool with four concurrent workers and a 60-second timeout per HTTP attempt. Uncertain outcomes are recorded as `unknown` instead of being automatically retried, helping applications avoid accidental duplicate delivery. Credentials are read from the component environment and never stored in queued jobs or email records.

Test mode is enabled by default, so developers can exercise the queue without sending real email.

Benefits

Use cases

how to send email from a Convex mutation without side effects on rollback

The convex-cloudflare-email component lets you call email.sendEmail() inside a Convex mutation. The enqueue is part of the mutation's transaction, so if the transaction aborts the email is never queued. This avoids the common problem of sending an email for a write that never committed.

how to add retry logic to Cloudflare email sending in Convex

The component uses convex-dev/workpool internally and retries HTTP 429 responses with exponential backoff, honoring Retry-After headers. You configure maxAttempts (1 to 10) and initialBackoffMs on the CloudflareEmail instance. Network failures and 5xx responses are marked unknown rather than retried blindly, because Cloudflare's send API has no documented idempotency guarantee.

how to deduplicate transactional emails in Convex

Pass an idempotencyKey string to email.sendEmail(). The convex-cloudflare-email component rejects a second enqueue with the same key if a job for that key already exists in the component. After a completed record is deleted by cleanup(), the key becomes available again, so retention period should match the deduplication window your application requires.

how to track email delivery status in a Convex backend

Call email.getStatus(ctx, id) from any query or action context. It returns the status string, attempt count, timestamps, and a result object containing delivered, queued, permanentBounces, and suppressedRecipients arrays when the send succeeded. The component does not consume Cloudflare Event Subscriptions, so delivery events beyond the initial API response are not reflected automatically.

Frequently asked questions

Does convex-cloudflare-email require a Cloudflare Worker to send email?

No. The convex-cloudflare-email component calls Cloudflare's Email Sending REST API directly from Convex actions. You need a Cloudflare account with an onboarded sending domain and an API token scoped to Email Sending: Edit, but no Worker deployment is required.

What happens if the HTTP request to Cloudflare times out or returns a 5xx error?

The convex-cloudflare-email component marks the email unknown rather than failed. Unknown status means an attempt may have reached Cloudflare but its result is uncertain. A watchdog marks abandoned in-progress attempts unknown after 15 minutes. You can call email.retryEmail() on an unknown email after checking Cloudflare logs, but you must pass acknowledgeDuplicateRisk: true because a duplicate send could occur.

How do I test email sending locally without real Cloudflare credentials?

The CloudflareEmail instance defaults to testMode: true. In test mode, sendEmail() completes with a test status without making any HTTP request or requiring CLOUDFLARE_ACCOUNT_ID or CLOUDFLARE_API_TOKEN. For unit tests in a consuming app, import register from convex-cloudflare-email/test and call register(t) after creating a convexTest instance.

Can I cancel an email after calling sendEmail()?

Yes, call email.cancelEmail(ctx, id) from a mutation or action while the email is in pending status. Once a worker has claimed the job and the status moves to sending, cancellation is no longer possible. The method returns a boolean indicating whether cancellation succeeded.

How does convex-cloudflare-email handle credential rotation?

Credentials are read from Convex environment variables when each send attempt runs, not when the email is enqueued. Updating CLOUDFLARE_ACCOUNT_ID or CLOUDFLARE_API_TOKEN with npx convex env set takes effect for all subsequent attempts, including retries of already-queued emails. No requeuing is needed after a credential rotation.

Links