How It Works
The R2 connector is a self-contained integration that runs entirely inside your Cloudflare account. ROOTKey publishes a Terraform module that you deploy once per bucket. The module wires a Cloudflare Worker to a Cloudflare Queue that receives R2 event notifications whenever a new object is created. When a new file is uploaded to the monitored bucket (and matches your filtering rules, if configured), the Worker reads the object directly from R2 via an in-cluster binding (zero egress) and streams it to the ROOTKey API authenticated with your Connector API Key. ROOTKey stores the file and anchors it on-chain, enabling both integrity verification and full file recovery in the event of corruption, ransomware, or accidental deletion.What This Module Creates in Your Cloudflare Account
Full transparency on what lands in your account when youterraform apply:
| Resource | Purpose | Cost impact |
|---|---|---|
cloudflare_workers_script | The connector itself (TypeScript bundled to ~6 KB ESM, deployed as a Workers module). | Free tier covers 100 K invocations/day; paid plan is 0.30/M after. |
cloudflare_workers_secret | Holds the ROOTKey Connector API Key. Encrypted at rest; never visible in plaintext through the dashboard or API after creation. | None (free with Workers). |
cloudflare_queue (×2) | Main events queue + dead-letter queue. R2 publishes object-created events to the main queue; the Worker consumes from it. | ~$0.40 per million operations (R2 event + Worker ack = ~2 ops). |
cloudflare_queue_consumer | Binds the Worker as the consumer of the main queue with max_retries = 5 and the DLQ as the failure destination. | None. |
cloudflare_r2_bucket_event_notification | Routes PutObject, CompleteMultipartUpload, and CopyObject events from your bucket into the queue. Supports server-side prefix filtering. | None. |
Infrastructure Impact Summary
Does this modify my R2 bucket?
Does this modify my R2 bucket?
Does this affect my other Cloudflare Workers or Queues?
Does this affect my other Cloudflare Workers or Queues?
name_suffix you provide plus a hash of the bucket name. Pre-existing Workers, Queues, R2 buckets, and Pages projects in the same account are not touched.Does the file content leave my Cloudflare account?
Does the file content leave my Cloudflare account?
https:// API URLs at plan time). Files are streamed from R2 to the ROOTKey API; the Worker never persists them anywhere else. Cloudflare bills $0 egress for this path.What happens to my API key?
What happens to my API key?
terraform apply with a new value. The Worker reads it from env.ROOTKEY_API_KEY at runtime.What happens if the ROOTKey API is unreachable?
What happens if the ROOTKey API is unreachable?
message.retry() and Cloudflare Queues automatically retries the message with exponential backoff up to 5 times. After exhaustion the message is moved to the dead-letter queue for human inspection. Permanent failures (oversize files, 4xx from ROOTKey) bypass the DLQ deliberately and surface via a stable log marker rootkey.event.dlq_terminal_failure for alerting.Can I roll this back?
Can I roll this back?
terraform destroy removes every resource the module created (Worker, secret, both queues, queue consumer, event notification). The R2 bucket itself is not deleted — it is your resource, not the module’s.Prerequisites
Before starting, ensure you have:- A Cloudflare account with R2 enabled.
- A pre-existing R2 bucket to monitor. The module does not create the bucket.
- A Cloudflare API token with permissions to manage Workers, Queues, R2 event notifications, and Workers Secrets in the target account. Generate one at dash.cloudflare.com → My Profile → API Tokens.
- Your Cloudflare Account ID (32-char hex, visible in the dashboard sidebar).
- Terraform v1.3 or later.
- Node.js 22+ on the machine running Terraform (used to compile the Worker at
terraform applytime).
API Token Permissions
The Cloudflare API token used to runterraform apply needs the following permissions on the target account:
| Permission | Why |
|---|---|
Workers Scripts: Edit | Create, update, and delete the Worker. |
Workers Secrets: Edit | Set the Connector API Key on the Worker. |
Queues: Edit | Create the main queue, the DLQ, and the consumer binding. |
R2 Bucket: Read + R2 Event Notifications: Edit | Read bucket metadata and configure the event notification. |
Configuration Fields
| Field | Required | Default | Description |
|---|---|---|---|
| Connector Name | Yes | — | A human-readable name to identify this connector in the dashboard. |
| Destination Vault | Yes | — | The ROOTKey vault where anchored files will be stored. |
| Cloudflare Account ID | Yes | — | 32-character hex Account ID. |
| Bucket Name | Yes | — | Exact name of the R2 bucket to monitor. |
| Name Suffix | Yes | — | 3–12 lowercase alphanumeric chars used to namespace the resources (e.g. acme or prod). |
| Prefix | No | "" | R2 key prefix filter. Empty means the entire bucket. Filtering happens server-side at the R2 event-notification layer. |
| ROOTKey API URL | No | https://api.rootkey.ai | ROOTKey API base URL. Must use https://. |
| Max file size (bytes) | No | 524288000 (500 MiB) | Objects larger than this are skipped with a structured log marker. |
| Tags | No | {} | Tags applied to the Worker script (queue/event-notification tagging is not currently supported by Cloudflare). |
Setup
The setup has a natural ordering dependency: the dashboard requires the Cloudflare Account ID and bucket name to create the connector, and the Worker requires the Connector API Key to call the ROOTKey API. The dashboard resolves this by generating a ready-to-run Terraform block with all values pre-filled.Create the R2 bucket (if you don't have one)
Generate a Cloudflare API token
Find your Cloudflare Account ID
Create the connector in the dashboard
Copy the Connector API Key and the Terraform block
- The Connector API Key.
- A ready-to-run Terraform block, pre-filled with your values.
Deploy the Terraform module
.tf file in an empty directory, export your Cloudflare API token, and apply:Reliability and observability
The connector leans on Cloudflare Queues’ native retry + DLQ semantics — there’s no in-Worker retry layer to debug.Retry behaviour
| Layer | Retries | When |
|---|---|---|
| Queue consumer | Up to 5 attempts with exponential backoff (native Cloudflare Queues behaviour) | On message.retry() — triggered by 429/5xx responses from ROOTKey or network errors. |
| Dead-letter queue | Final destination after retry exhaustion | Operators inspect via Cloudflare API or dashboard. |
| Permanent failure short-circuit | PermanentError is caught and acked immediately, bypassing the DLQ | 4xx from ROOTKey, oversize files, or objects that no longer exist in R2. Surfaced via rootkey.event.dlq_terminal_failure log marker. |
Idempotency
Every upload to the ROOTKey API carries three headers extracted from the R2 object:| Header | Source |
|---|---|
x-rootkey-source-bucket | R2 event bucket |
x-rootkey-source-key | R2 event object.key |
x-rootkey-source-etag | Live R2 object ETag (read at processing time — preferred over event payload) |
What to monitor
| Signal | What it means | How to alert |
|---|---|---|
| DLQ queue depth > 0 for more than ~10 min | Transient failures exhausted the retry budget. | Cloudflare dashboard → Queues → depth metric. |
Worker logs contain rootkey.event.dlq_terminal_failure | A message hit a permanent error and was acked without retrying. | Workers Logpush + alert on the marker. |
| Worker invocation error rate > 0 | Recurring runtime errors. | Workers Analytics → error count. |
ROOTKey dashboard connector status ERROR | API rejected uploads (invalid key, vault deleted, quota). | Email/Slack via your dashboard notification settings. |
Security considerations
The module ships with a defensive default posture out of the box:- API key in a Workers Secret, not in code. Encrypted at rest, not readable after creation. Rotation =
terraform applywith a new value. - HTTPS-only. The module rejects non-
https://API URLs at plan time. - In-cluster R2 read. The Worker reads R2 objects via a binding — no HTTP egress, no public network exposure for the data path.
- Bucket-scoped binding. The Worker’s R2 binding is scoped to one bucket. If the Worker is ever compromised, the blast radius is one bucket — not the customer’s whole R2 footprint.
- No additional IAM model to manage. Unlike the AWS/Azure connectors, there is no role or App Registration to audit; the Worker can only use the bindings declared in Terraform.
Filtering Rules
To anchor only specific files (e.g., only PDFs, or exclude temporary files), configure Filtering Rules on the connector after creation. Rules apply on the ROOTKey side — files filtered out are not stored in the vault. You can also restrict at the R2 side by setting the Prefix field, which translates into the event-notification server-side filter: events for objects whose key does not start with the prefix never invoke the Worker.Troubleshooting
Objects are uploaded to R2 but nothing reaches ROOTKey
Objects are uploaded to R2 but nothing reaches ROOTKey
- Event notification is configured. Cloudflare dashboard → R2 → your bucket → Settings → Event notifications must show the
rootkey-r2-events-*queue subscribed. - DLQ depth. Cloudflare dashboard → Queues → your DLQ. Non-zero means transient errors exhausted retries.
- Worker logs.
wrangler tail $(terraform output -raw worker_name)— look for terminal-failure markers. - Filter prefix. If you set
prefix, events for keys not matching the prefix never reach the Worker.
Connector status is ERROR in the dashboard
Connector status is ERROR in the dashboard
- The Connector API Key was modified or deleted as a Workers Secret. Retrieve a new key by deleting and recreating the connector.
- The destination vault was deactivated or deleted. Reactivate it or change the connector’s vault binding.
A large file fails to upload
A large file fails to upload
- Stay under 500 MiB (recommended).
- Move to Cloudflare Workers Unbound or the Standard pricing tier with more headroom and adjust
max_file_size_bytesaccordingly.
How do I rotate the Connector API Key?
How do I rotate the Connector API Key?
- In the ROOTKey dashboard, delete the connector and create a new one (the Cloudflare resources can be reused).
- Update the
rootkey_api_keyTerraform variable with the new key. - Run
terraform apply— the module overwrites the Workers Secret with the new value, and the Worker picks it up on the next invocation.
Can I monitor multiple R2 buckets?
Can I monitor multiple R2 buckets?
name_suffix + a hash of the bucket name. Reuse the same Cloudflare account.What happens if a message is in the DLQ?
What happens if a message is in the DLQ?
- If the cause was transient (e.g., ROOTKey API was down): re-publish the message to the main queue.
- If the cause was a permanent issue (key rotated incorrectly, vault deleted): resolve the root cause first, then replay.
rootkey.event.dlq_terminal_failure markers in Worker logs.Source code
The Terraform module and Worker source live in the public rootkey-ai/rootkey-connectors repository under ther2/ directory. The code is licensed under the Apache License 2.0 — you are free to fork it, audit it, or pin to a specific commit if your change-management process requires it.
→ Back to Connectors Overview

