Skip to content

Stage Cloudflare Email delivery events

Prepare Tinbox's default-off Cloudflare Email Sending lifecycle Queue, DLQ, and trust allowlists locally without creating a subscription or enabling the feature.

Cloudflare Email Sending can publish six outbound lifecycle events to a Queue: delivered, deferred, bounced, failed, rejected, and complained. Tinbox has a strict consumer for those raw versioned events, but it ships behind the default-off cloudflareEmailDeliveryEvents release flag.

The standard local scaffold stages the infrastructure shape without activating it:

"queues": {
  "producers": [
    // NEWSLETTER_QUEUE and PROJECTION_QUEUE only — never EMAIL_EVENTS_QUEUE.
  ],
  "consumers": [
    {
      "queue": "tinbox-acme-email-events",
      "max_batch_size": 10,
      "max_batch_timeout": 30,
      "max_concurrency": 1,
      "max_retries": 5,
      "retry_delay": 60,
      "dead_letter_queue": "tinbox-acme-email-events-dlq"
    }
  ]
},
"vars": {
  "CLOUDFLARE_ACCOUNT_ID": "",
  "CLOUDFLARE_EMAIL_EVENT_SUBSCRIPTION_IDS": ""
}

The lifecycle queue is deliberately consumer-only. Do not add a Worker Queue producer binding for either it or its DLQ. The Cloudflare Event Subscription is the only configured producer; account and subscription ids are then checked again inside the consumer before any D1 mutation.

Prepare it locally

From the tenant project—not from GitHub automation—inspect and provision the declared Queue resources:

tinbox setup --dry-run
tinbox setup

The dry run performs no authentication, writes, or Cloudflare calls. The real setup creates the two referenced Queue resources through Wrangler and fills CLOUDFLARE_ACCOUNT_ID from the authenticated account. It does not create an Event Subscription, deploy, or enable the release flag unless you separately request those actions.

Leave CLOUDFLARE_EMAIL_EVENT_SUBSCRIPTION_IDS empty while staging. Empty, missing, or mismatched trust configuration makes every event retry without a D1 write.

Create the subscription only at rollout

Once a Tinbox release explicitly enables the consumer for your build:

  1. In Cloudflare, open Queues → tinbox-<org>-email-events → Subscriptions → Subscribe to events.
  2. Select the Email Sending source, the verified sending domain, and all six lifecycle events.
  3. Copy the returned subscription id into CLOUDFLARE_EMAIL_EVENT_SUBSCRIPTION_IDS. Multiple domain-scoped subscriptions are comma-separated.
  4. Redeploy locally with the tenant's documented tinbox deploy command.
  5. Send one transactional test and verify a matched lifecycle row before expanding traffic.

Cloudflare subscriptions are scoped to one apex or verified sending subdomain. Use a dedicated queue per Tinbox tenant. Sharing it with arbitrary Queue API producers weakens the authenticity boundary because event bodies do not carry a Tinbox signature.

Failure and DLQ behavior

Malformed, unknown-version, untrusted, and unmatched events retry. Unmatched events may simply have beaten the send-ledger update, so retrying preserves the chance to correlate them. After five retries, Cloudflare moves the body to tinbox-<org>-email-events-dlq.

Tinbox intentionally does not attach its Worker as a DLQ consumer: poison or spoofed bodies remain quarantined for explicit operator inspection instead of being accidentally interpreted by another queue branch. A Cloudflare DLQ with no active consumer retains messages for four days, so inspect or pull them before that window expires.

References