/** * For more details on how to configure Wrangler, refer to: * https://developers.cloudflare.com/workers/wrangler/configuration/ */ { "$schema": "node_modules/wrangler/config-schema.json", "name": "reccado", "compatibility_date": "2026-06-30", "compatibility_flags": ["nodejs_compat"], "main": "src/server.ts", "observability": { "enabled": true }, "upload_source_maps": true, // Production should not be reachable on the shared *.workers.dev subdomain. // Front public installs with a custom domain and Cloudflare Access. "workers_dev": false, "vars": { "MAIL_FROM_ADDRESS": "noreply@mail.example.com", // Mailbox Durable Objects are pinned to Cloudflare's EU jurisdiction. // Declared rather than assumed because workerd does not implement // jurisdictions, so `.dev.vars` overrides this to "none" locally and the // guarantee only exists in a deployed Worker. See src/lib/mailbox-stub.ts. "MAILBOX_JURISDICTION": "eu" }, "triggers": { "crons": ["0 * * * *"] }, "send_email": [ { "name": "EMAIL" } ], "durable_objects": { "bindings": [ { "name": "MAILBOX_DO", "class_name": "MailboxDurableObject" } ] }, "r2_buckets": [ { "binding": "MAIL_OBJECTS", "bucket_name": "inbox-mcp-raw", // Raw MIME is the actual message content at rest — more so than D1, // which only holds snippets — so the residency story is incomplete // without it. A fork that does not want EU residency changes this and // MAILBOX_JURISDICTION together; changing one alone leaves a half // guarantee, which is worse than none because it reads as done. "jurisdiction": "eu" } ], "queues": { "producers": [ { "binding": "INBOUND_EMAIL_QUEUE", "queue": "inbox-mcp-inbound" }, { "binding": "EMAIL_EVENTS_QUEUE", "queue": "inbox-mcp-email-events" }, { "binding": "NOTIFY_QUEUE", "queue": "inbox-mcp-notify" } ], "consumers": [ { "queue": "inbox-mcp-inbound", "dead_letter_queue": "inbox-mcp-inbound-dlq", "max_batch_size": 5, "max_batch_timeout": 2, "max_retries": 3 }, { "queue": "inbox-mcp-email-events", "dead_letter_queue": "inbox-mcp-email-events-dlq", "max_batch_size": 10, "max_batch_timeout": 2, "max_retries": 3 }, { "queue": "inbox-mcp-notify", "dead_letter_queue": "inbox-mcp-notify-dlq", // Telegram takes about one message per second per chat, so a large // batch would spend most of its invocation being rate-limited; five // keeps a burst moving without stalling the consumer. The 1s timeout // is the point of the queue — a notification that waits to fill a // batch is a notification that arrived late. max_retries is higher // than ingest's because the failures here are Telegram outages and // 429s, which pass, and a dropped push is a signal the operator // never gets back. "max_batch_size": 5, "max_batch_timeout": 1, "max_retries": 5 }, // The three dead-letter queues are consumed by the same terminal // tombstone handler: a dead message must land somewhere durable and // named instead of evaporating when Queues retention expires. These // entries deliberately declare no dead_letter_queue of their own — // this is the end of the line, and a DLQ for the DLQ would only move // the silence one hop away. Batches are large and unhurried because // nothing downstream is waiting; the retries exist only to ride out a // D1 blip while writing the record. { "queue": "inbox-mcp-inbound-dlq", "max_batch_size": 10, "max_batch_timeout": 5, "max_retries": 5 }, { "queue": "inbox-mcp-email-events-dlq", "max_batch_size": 10, "max_batch_timeout": 5, "max_retries": 5 }, { "queue": "inbox-mcp-notify-dlq", "max_batch_size": 10, "max_batch_timeout": 5, "max_retries": 5 } ] }, "d1_databases": [ { "binding": "INDEX_DB", "database_name": "inbox-mcp-index", // Forkers: create your own D1 database (`wrangler d1 create // inbox-mcp-index`) and paste its id here. This is a template // value, not a real database. "database_id": "", "migrations_dir": "migrations/d1" } ], "migrations": [ { "tag": "v1", "new_sqlite_classes": ["MailboxDurableObject"] } ], "env": { "dev": { "name": "reccado-dev", // Off, like production. Not for secrecy -- the perimeter is in the worker // now, so both hostnames were equally protected -- but because the // deployment learns its canonical origin from whichever host an // authenticated request last arrived on, and that origin is what MCP // tokens are bound to. A stale workers.dev tab loading one mailbox was // enough to silently re-point it away from the custom domain. "workers_dev": false, // Who may drive the bot is still a trust declaration a human makes — but a // personal Telegram user id committed to a public repository, changeable // only by redeploy, was a bad way to write one down. The declaration moved // to owner_identities in D1, made by sending `/start `. // TELEGRAM_ALLOWED_USER_IDS survives as an emergency bootstrap and is // deliberately not set here. "vars": { "MAIL_FROM_ADDRESS": "noreply@send.imsanti.dev", // imsanti.dev is the zone apex, so hello@imsanti.dev sends as itself. Ship // this only after `wrangler email sending list` shows imsanti.dev enabled: // a domain listed here that Cloudflare has not accepted makes every send from // that mailbox fail instead of falling back to MAIL_FROM_ADDRESS. "MAIL_SENDING_DOMAINS": "imsanti.dev,send.transcribo.es,transcribo.es", "MAILBOX_JURISDICTION": "eu" }, "triggers": { "crons": ["0 * * * *"] }, "send_email": [ { "name": "EMAIL" } ], "durable_objects": { "bindings": [ { "name": "MAILBOX_DO", "class_name": "MailboxDurableObject" } ] }, "r2_buckets": [ { "binding": "MAIL_OBJECTS", "bucket_name": "inbox-mcp-raw-dev-eu", "jurisdiction": "eu" } ], "queues": { "producers": [ { "binding": "INBOUND_EMAIL_QUEUE", "queue": "inbox-mcp-inbound-dev" }, { "binding": "EMAIL_EVENTS_QUEUE", "queue": "inbox-mcp-email-events-dev" }, { "binding": "NOTIFY_QUEUE", "queue": "inbox-mcp-notify-dev" } ], "consumers": [ { "queue": "inbox-mcp-inbound-dev", "dead_letter_queue": "inbox-mcp-inbound-dlq-dev", "max_batch_size": 5, "max_batch_timeout": 2, "max_retries": 3 }, { "queue": "inbox-mcp-email-events-dev", "dead_letter_queue": "inbox-mcp-email-events-dlq-dev", "max_batch_size": 10, "max_batch_timeout": 2, "max_retries": 3 }, { "queue": "inbox-mcp-notify-dev", "dead_letter_queue": "inbox-mcp-notify-dlq-dev", // Mirrors prod: small batches because Telegram is ~1 msg/s per // chat, a 1s timeout because a batched-up notification is a late // one, and more retries than ingest because 429s and outages pass // while a dropped push never comes back. "max_batch_size": 5, "max_batch_timeout": 1, "max_retries": 5 }, // Terminal tombstone consumers, as in prod: a dead message gets a // durable, named row in ops_events rather than expiring unseen. // No dead_letter_queue on purpose — the line ends here. { "queue": "inbox-mcp-inbound-dlq-dev", "max_batch_size": 10, "max_batch_timeout": 5, "max_retries": 5 }, { "queue": "inbox-mcp-email-events-dlq-dev", "max_batch_size": 10, "max_batch_timeout": 5, "max_retries": 5 }, { "queue": "inbox-mcp-notify-dlq-dev", "max_batch_size": 10, "max_batch_timeout": 5, "max_retries": 5 } ] }, "d1_databases": [ { "binding": "INDEX_DB", // Created with `--jurisdiction eu`, which is a property of the // database rather than of this binding, so there is nothing here to // declare and nothing to keep in sync. It is named `-eu` precisely // because that fact is otherwise invisible from the config. "database_name": "inbox-mcp-index-dev-eu", // Forkers: create your own dev D1 (`wrangler d1 create // --update-config --binding INDEX_DB --env dev`) and this id gets // filled in for you. The all-zero UUID is a valid-format placeholder // (kept parseable because local `vitest` uses `environment: "dev"`); // remote dev deploys still need a real id. "database_id": "ac9f17a6-3159-4fd5-8f81-cd839d6aa043", "migrations_dir": "migrations/d1" } ], "migrations": [ { "tag": "v1", "new_sqlite_classes": ["MailboxDurableObject"] } ] } } }