Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Cloudflare Email

Durable email sending with Cloudflare Email Service for Bijection.

Bijection source
git clone https://github.com/bijectionhq/cloudflare-email.git
cd cloudflare-email
git checkout 919f51e01fee9090809ba9991f1fd4153cd2fddd

Bijection repository access is required. Request access.

Description

Durable email sending with Cloudflare Email Service for Bijection.

Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. Exercise the component in your deployment according to your project’s verification rules.

Open source

Maintained source: bijectionhq/cloudflare-email. Based on adamtrip-solutions/convex-cloudflare-email. License: MIT. Consult the repository for license terms, notices, and contributor attribution.

From the upstream publisher

Durable email sending with Cloudflare Email Service for Convex.

Explore the demo ↗

Upstream overview

From the upstream package documentation.

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.

Use cases

From the upstream package documentation.

  • Transactional email: Enqueue welcome messages, password resets, and notifications inside a Convex mutation. If the surrounding transaction fails, the enqueue rolls back with it.
  • Duplicate-job prevention: Reuse an idempotency key when application code repeats a request, preventing another email job while the original record exists.
  • Send-status tracking: Query pending, sending, sent, test, failed, unknown, and canceled states without implementing a separate email-processing state machine.
  • Provider throttling: Handle Cloudflare’s HTTP 429 responses with bounded exponential backoff that respects Retry-After.
  • Cancellation: Cancel a pending email before a worker claims it, such as when a welcome message is no longer needed.
  • Development and testing: Run simulated sends and use the provided convex-test helper to test application integrations without sending external email.

How it works

From the upstream package documentation.

Register the component in convex/convex.config.ts using app.use(cloudflareEmail). For real sending, declare CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN in the app’s environment configuration and pass their references into the component through the env option. Run npx convex dev to generate the component references.

Create a server-side instance with new CloudflareEmail(components.cloudflareEmail, options). Test mode defaults to true and completes simulated sends with test status. Real sending requires testMode: false, valid credentials, and a Cloudflare domain configured for email sending. Credentials are read when each attempt runs, so queued messages use the current token after rotation.

Call email.sendEmail(ctx, options) from a mutation or action to enqueue a message and receive a branded EmailId. Enqueueing inside a mutation participates in the caller’s transaction. Reusing an idempotencyKey with the same payload and configuration returns the existing email ID; reusing it with different content or configuration throws. Keys apply within one installed component instance.

The component processes jobs through @convex-dev/workpool with four concurrent workers. Each attempt makes one HTTP request with a 60-second timeout. Only HTTP 429 responses are automatically retried, using exponential backoff starting at 30 seconds by default and respecting longer Retry-After delays. maxAttempts includes the initial attempt, defaults to three, and accepts values from one to ten.

Network failures, HTTP 408/5xx responses, and unusable successful responses become unknown, since Cloudflare may have accepted the message. A watchdog also marks abandoned attempts unknown after 15 minutes.

Use email.getStatus(ctx, id) to inspect the current send status and recipient results. A sent status does not guarantee inbox delivery. Use email.cancelEmail(ctx, id) to cancel before a worker claims the message, or email.retryEmail(ctx, id) to retry a failed send. Retrying an unknown outcome requires { acknowledgeDuplicateRisk: true }.

Call email.cleanup(ctx) from a cron or maintenance mutation. By default, it removes up to 20 eligible sent, test, failed, or canceled records older than seven days. Unknown outcomes remain for investigation. Deleting a record also removes its idempotency key, so retention determines the deduplication window.

The register(t) helper from convex-cloudflare-email/test integrates the component with convex-test. Applications should authenticate callers and check ownership before exposing sending or management operations to clients.

Upstream package FAQ

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.

Use with agents and CLI

Give your coding agent the component’s source identity and setup guidance.

agent-prompt.md
Help me integrate Cloudflare Email with Bijection.

Read: https://bijection.com/components/cloudflare-email/SKILL.md

Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.

Upstream README.md

View repository ↗

Documentation from adamtrip-solutions/convex-cloudflare-email, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.

Cloudflare Email for Convex

A Convex component for sending email through Cloudflare Email Service's beta REST API. Enqueue an email inside a mutation, track its status, and cancel it before sending starts.

This package is an initial beta implementation. It has been tested with mocked Cloudflare responses and a local Convex deployment. Real sending requires your Cloudflare account and an onboarded sending domain.

Interactive demo

Try the hosted demo to send a simulated email, watch its status, and repeat the request to verify deduplication. Demo source and setup. No external email is sent.

Install

Requires Node.js 22 or newer and Convex 1.45 or newer. Install the beta from npm:

npm install convex-cloudflare-email@beta

The npm package is in beta. Use @beta to select the beta channel. Releases will remain on 0.x.x until the stable 1.0.0 release.

Register the component in convex/convex.config.ts. Component environment variables keep credentials out of queued jobs and database rows.

import { defineApp } from "convex/server";
import { v } from "convex/values";
import cloudflareEmail from "convex-cloudflare-email/convex.config";

const app = defineApp({
  env: {
    CLOUDFLARE_ACCOUNT_ID: v.optional(v.string()),
    CLOUDFLARE_API_TOKEN: v.optional(v.string()),
  },
});
app.use(cloudflareEmail, {
  env: {
    CLOUDFLARE_ACCOUNT_ID: app.env.CLOUDFLARE_ACCOUNT_ID,
    CLOUDFLARE_API_TOKEN: app.env.CLOUDFLARE_API_TOKEN,
  },
});
export default app;

Run npx convex dev to generate the component references.

Send an email

// convex/email.ts
import { CloudflareEmail, vEmailId } from "convex-cloudflare-email";
import { components } from "./_generated/api";
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";

const email = new CloudflareEmail(components.cloudflareEmail);

export const sendWelcome = internalMutation({
  args: { userId: v.string(), to: v.string() },
  returns: vEmailId,
  handler: async (ctx, { userId, to }) => {
    return email.sendEmail(ctx, {
      from: { address: "hello@yourdomain.com", name: "Your app" },
      to,
      subject: "Welcome",
      text: "Thanks for joining.",
      html: "<p>Thanks for joining.</p>",
      idempotencyKey: `welcome:${userId}`,
    });
  },
});

The default is testMode: true. A test send ends in test status without contacting Cloudflare or requiring credentials. This is a local simulation, not a deliverability test. To send real mail, onboard your domain through Cloudflare Email Sending, create a token with Email Sending: Edit, and configure the app's environment variables:

npx convex env set CLOUDFLARE_ACCOUNT_ID your_32_character_account_id
npx convex env set CLOUDFLARE_API_TOKEN your_api_token

Then opt into real sends:

const email = new CloudflareEmail(components.cloudflareEmail, {
  testMode: false,
  maxAttempts: 3,
  initialBackoffMs: 30_000,
});

Credentials are read when each attempt runs, so queued emails use the current token after rotation. Configure separate component instances if you need separate accounts.

Input

sendEmail(ctx, options) returns a branded EmailId. It works from mutations and actions. Within a mutation, the enqueue rolls back if the caller's transaction fails. Multiple calls from an action are separate transactions.

FieldType and behavior
fromPlain email string or { address, name? }
to, cc, bccOne address or an array of addresses. At least one recipient overall.
subjectString, up to 998 characters, without line breaks
text, htmlAt least one nonempty body
replyToOne plain or named address
headersRecord of header names to string values
attachmentsArray of base64 attachments described below
idempotencyKeyOptional nonempty string, up to 256 characters

There can be at most 50 recipients combined. Use a named address object instead of a string such as Support <support@example.com>. Cloudflare validates sender domains, supported custom headers, suppression rules, and other provider restrictions. See its header reference.

Attachments require filename, MIME type, base64 content, and disposition: "attachment". Inline images use disposition: "inline" and a contentId, referenced in HTML as cid:logo.

attachments: [
  {
    filename: "logo.png",
    type: "image/png",
    content: base64Png,
    disposition: "inline",
    contentId: "logo",
  },
];

This version limits the entire JSON email payload to 512 KiB, including base64 content, to leave room within Convex's document limit. That is lower than Cloudflare's message size limit. Large attachments and raw MIME sending are not supported yet.

Status and controls

Call these methods from your app's server functions:

await email.getStatus(ctx, id); // Query or action context; null if deleted.
await email.cancelEmail(ctx, id); // Mutation or action; boolean.
await email.retryEmail(ctx, id); // Failed emails only, unless acknowledged below.
await email.retryEmail(ctx, id, { acknowledgeDuplicateRisk: true });
await email.cleanup(ctx); // Deletes at most 20 completed rows older than 7 days.

getStatus returns the component ID, status, attempt count for the current run, timestamps, and any result or error. It omits the email body and configuration. Exported vEmailStatus and vEmailId validators can validate application wrappers. Authenticate and check ownership in those wrappers before exposing status, retries, or sending to clients. Recipient results contain email addresses.

StatusMeaning
pendingWaiting for an attempt, including rate-limit backoff. Can be canceled.
sendingClaimed by a worker. Cancellation is too late.
sentCloudflare returned a valid successful response. Inspect recipient results.
testSimulated send. No HTTP request occurred.
failedConfiguration or request rejection, exhausted throttling attempts, or a job that failed before claiming.
unknownAn attempt may have reached Cloudflare, but its result is uncertain.
canceledCanceled before claim.

A successful result contains delivered, queued, permanentBounces, and suppressedRecipients arrays, plus messageId when returned. sent does not mean every recipient received the email. The component preserves the send-time snapshot. Cloudflare handles further delivery attempts for recipients in queued.

Retries and deduplication

The component uses @convex-dev/workpool with four concurrent workers per installed instance. It automatically retries HTTP 429 rejections using exponential backoff and the larger Retry-After delay. maxAttempts includes the first attempt and must be between 1 and 10. Backoff starts at 30 seconds by default. A requested delay exceeding one day stops the run as failed instead of retrying early.

There is no documented idempotency guarantee for Cloudflare's send operation. The component therefore makes a single HTTP request per attempt, with a 60-second timeout. Network failures, HTTP 408/5xx, and unusable successful responses become unknown. A watchdog marks abandoned attempts unknown after 15 minutes. Generic workpool retries are disabled.

An enqueue key prevents duplicate jobs within one installed component. Reusing a key with a different payload or configuration throws. This does not provide exactly-once delivery across an HTTP request and a database write. Check Cloudflare logs before acknowledging an unknown retry. A manual retry starts a new run and resets its attempt count.

The component never resends a successful request to retry its bounced or queued recipients. Such a repeat could duplicate email for recipients already delivered.

Retention

Email bodies and recipient data stay in the component database until cleanup. Call cleanup from an application cron or a maintenance mutation. It processes one batch per call, accepts olderThanMs of at least one day and limit from 1 to 20, and returns the number deleted. It removes old sent, test, failed, and canceled records. unknown records are retained for investigation.

Deleting a record also deletes its enqueue key. Reusing that key after cleanup can send another email. Set retention to match the deduplication period your application needs. Workpool maintains its own job records; those contain component IDs and generation numbers, not message bodies or credentials.

Development and tests

npm ci
npm run check
CONVEX_AGENT_MODE=anonymous npx convex dev --once
npx convex run email:sendExample '{}'
# Use the returned ID:
npx convex run email:status '{"id":"RETURNED_EMAIL_ID"}'
npm run codegen

The example always uses test mode. Keep the local backend running with npm run dev when making CLI calls. Unit tests mock every HTTP request and cover response parsing, failures, throttling, claim/cancel races, deduplication, transaction rollback, stale completions, cleanup, and execution through the nested workpool.

For tests in a consuming app:

import { convexTest } from "convex-test";
import { register } from "convex-cloudflare-email/test";
import schema from "./schema";

const t = convexTest(schema, import.meta.glob("./**/*.ts"));
register(t); // Pass the component's name as the second argument if renamed.

Differences from the Resend component

The server wrapper and transactional queue follow the pattern of Convex's Resend component. This implementation targets Cloudflare's REST API and uses its recipient results. It does not include Resend templates, batch sending, open/click tracking, callbacks, or webhooks.

Cloudflare delivery events use Event Subscriptions and Queues. Consuming those events to update delivery status is outside this initial version. Sending itself requires no Cloudflare Worker.

Contributing

Contributions are welcome, including bug reports, tests, documentation, and code. See CONTRIBUTING.md for setup and the pull request process. Report vulnerabilities through the private channel in SECURITY.md.

Releases and license

GitHub Actions validates each pull request and publishes approved releases directly to npm. Maintainers should follow the release guide. The component is available under the MIT license.

Keywords