main = "src/worker.ts" # 2026-03-24 is the minimum for the Browser Rendering `env.BROWSER.quickAction` binding method. compatibility_date = "2026-03-24" workers_dev = true compatibility_flags = ["nodejs_compat"] # Static Assets: the default welcome snapshot (assets/welcome-snapshot.json) is read at seed # time via the ASSETS binding, so it is not bundled into the worker script. run_worker_first # keeps the worker in front of every request — the directory is never served publicly; the # worker only reads the asset through the binding. `assets` is a non-inheritable key and this # file overrides bindings per environment, so it is repeated in each [env.*] block below. [assets] directory = "assets" binding = "ASSETS" run_worker_first = true [dev] port = 8787 ip = "0.0.0.0" # these migrations are append-only. you can't change them. if you do need to change something, do so # by creating new migrations. # IMPORTANT: when adding a new migration here, also add it to the [[env.preview.migrations]] block below # env-level migrations fully replace the top-level list, so preview won't inherit new entries. [[migrations]] tag = "v1" # Should be unique for each entry new_classes = ["TLDrawDurableObject"] [[migrations]] tag = "v2" new_classes = ["TLProWorkspaceDurableObject"] [[migrations]] tag = "v3" deleted_classes = ["TLProWorkspaceDurableObject"] [[migrations]] tag = "v4" new_classes = ["TLAppDurableObject"] [[migrations]] tag = "v5" new_sqlite_classes = ["TLPostgresReplicator"] new_classes = ["TLUserDurableObject"] [[migrations]] tag = "v6" new_sqlite_classes = ["TLLoggerDurableObject"] [[migrations]] tag = "v7" new_sqlite_classes = ["TLStatsDurableObject"] [[migrations]] tag = "v8" deleted_classes = ["TLAppDurableObject"] [[migrations]] tag = "v9" new_sqlite_classes = ["TLFileDurableObject"] [[migrations]] tag = "v10" deleted_classes = ["TLDrawDurableObject"] [[migrations]] tag = "v11" new_sqlite_classes = ["TLFileEffectProcessor"] # v12 deletes the legacy sync engine's DO classes (their bindings were removed in the # previous deploy; delete-class must not share a deploy with binding removal). Their no-op # stub exports in worker.ts stay forever — staging/prod migration history references the # classes, so removing the exports breaks deploys (see #8124). NOT mirrored in the preview # list: preview workers deploy fresh and never created these classes (see below). [[migrations]] tag = "v12" deleted_classes = ["TLPostgresReplicator", "TLUserDurableObject", "TLStatsDurableObject"] # preview workers are fresh deploys, so CF rejects v10's delete-class for # TLDrawDurableObject (code 10074). skip v1 and v10 here; env-level migrations # fully replace the top-level list. # v5 (TLPostgresReplicator + TLUserDurableObject) and v7 (TLStatsDurableObject) are also # skipped. Their delete-class migration can only ever go in the top-level list, because a # fresh preview worker replaying create+delete for one class hits the same 10074 as v1/v10 # above. Skipping the creates here keeps preview consistent with that: the classes are # never created, so preview needs neither the delete nor the classes. [[env.preview.migrations]] tag = "v2" new_classes = ["TLProWorkspaceDurableObject"] [[env.preview.migrations]] tag = "v3" deleted_classes = ["TLProWorkspaceDurableObject"] [[env.preview.migrations]] tag = "v4" new_classes = ["TLAppDurableObject"] [[env.preview.migrations]] tag = "v6" new_sqlite_classes = ["TLLoggerDurableObject"] [[env.preview.migrations]] tag = "v8" deleted_classes = ["TLAppDurableObject"] [[env.preview.migrations]] tag = "v9" new_sqlite_classes = ["TLFileDurableObject"] [[env.preview.migrations]] tag = "v11" new_sqlite_classes = ["TLFileEffectProcessor"] [[analytics_engine_datasets]] binding = "MEASURE" #################### Environment names #################### # dev should never actually get deployed anywhere [env.dev] name = "dev-tldraw-multiplayer" # The top-level compatibility_date (2026-03-24, required for the Browser Rendering `quickAction` # binding on deployed environments) is newer than the workerd bundled with our pinned wrangler, so # `wrangler dev` — and the process-compose e2e stack that runs it — would refuse to start. Local dev # doesn't drive real Browser Run (the `quickAction` binding is remote-only), so pin dev to a date the # local workerd supports. Bump this once the pinned wrangler ships a workerd that supports 2026-03-24. compatibility_date = "2025-06-05" [env.dev.vars] BOTCOM_POSTGRES_CONNECTION_STRING = "postgresql://user:password@127.0.0.1:6543/postgres" BOTCOM_POSTGRES_POOLED_CONNECTION_STRING = "postgresql://user:password@127.0.0.1:6432/postgres" TLDRAW_ENV = "development" MULTIPLAYER_SERVER = "http://localhost:3000" MCP_SCREENSHOT_RENDER_ORIGIN = "http://localhost:3000" # The MCP server's public URL, and the resource identifier it advertises in protected resource # metadata and in the WWW-Authenticate challenge. Not compared against an incoming token: Clerk stamps # no `aud`, so there is no audience binding to check. Points at the client dev server because that is # what proxies /api and /.well-known through to this worker. MCP_SERVER_URL = "http://localhost:3000/api/app/mcp" # Kill switch for the MCP screenshot server (POST /app/mcp). Read per request, so setting it to # anything but "true" — here or straight in the Cloudflare dashboard — takes the endpoint down # without a rebuild. A dashboard edit lasts until the next deploy overwrites it from this file. MCP_SERVER_ENABLED = "true" # local-only signing secret; deployments get a real one via deploy vars MCP_SCREENSHOT_TOKEN_SECRET = "local-dev-thumbnail-render-secret" # Browser Run is unreachable from local dev (see the BROWSER binding note below), so screenshots go # to the client dev server, which drives Playwright. Deployed environments leave this unset and use # the BROWSER binding. LOCAL_SCREENSHOT_SERVICE_URL = "http://localhost:3000/__screenshot" # we don't have a hard-coded name for preview. we instead have to generate it at build time and append it to this file. # staging is the same as a preview on main: [env.staging] name = "main-tldraw-multiplayer" workers_dev = false [env.staging.vars] HEALTH_CHECK_DB_SIZE_THRESHOLD_GB = "4" HEALTH_CHECK_CHANGELOG_SIZE_THRESHOLD_MB = "1024" HEALTH_CHECK_WAL_SIZE_THRESHOLD_MB = "1024" MCP_SCREENSHOT_RENDER_ORIGIN = "https://staging.tldraw.com" MCP_SERVER_URL = "https://staging.tldraw.com/api/app/mcp" MCP_SERVER_ENABLED = "true" MCP_SEARCH_NAME_MATCHING_ENABLED = "true" # Staging joins previews in trialling live-canvas capture (see THUMBNAIL_RENDER_LIVE_CAPTURE in # types.ts). Previews are too short-lived and too lightly used to produce the render_page_timings # volume the trial is judged on; production stays on the export until it is. THUMBNAIL_RENDER_LIVE_CAPTURE = "true" # production gets the proper name [env.production] name = "tldraw-multiplayer" workers_dev = false [env.production.vars] HEALTH_CHECK_DB_SIZE_THRESHOLD_GB = "10" HEALTH_CHECK_CHANGELOG_SIZE_THRESHOLD_MB = "1024" HEALTH_CHECK_WAL_SIZE_THRESHOLD_MB = "2048" MCP_SCREENSHOT_RENDER_ORIGIN = "https://www.tldraw.com" MCP_SERVER_URL = "https://www.tldraw.com/api/app/mcp" MCP_SERVER_ENABLED = "true" # Off while the unindexed `name ILIKE '%term%'` scan it drives is still being watched: it is the one # part of search no index reaches, so a term matching nothing reads every board in the caller's # scope. Off, the tool lists rather than searches and advertises no query — it does not accept a # query and ignore it. Unset reads as on, which is what previews and local dev get. MCP_SEARCH_NAME_MATCHING_ENABLED = "false" #################### Routing #################### # no custom routes for previews and development [[env.staging.routes]] zone_name = "tldraw.com" pattern = "staging.tldraw.com/api/*" # OAuth protected resource metadata for the MCP server. RFC 9728 puts it at the resource's *origin*, # under /.well-known/oauth-protected-resource + the resource path, so it falls outside the /api/* # pattern above and needs its own route. Without it a client fetches the metadata URL, gets the SPA's # index.html, and never discovers where to authenticate — a failure that logs nothing on our side. [[env.staging.routes]] zone_name = "tldraw.com" pattern = "staging.tldraw.com/.well-known/oauth-protected-resource/*" # The same path without the trailing slash, which the pattern above does not match. Clients fall back # to the bare URL when the path-derived one 404s, so without this the fallback lands on the SPA. [[env.staging.routes]] zone_name = "tldraw.com" pattern = "staging.tldraw.com/.well-known/oauth-protected-resource" # The MCP Server Card's `.well-known` alias, outside /api/* for the same reason as the metadata above. [[env.staging.routes]] zone_name = "tldraw.com" pattern = "staging.tldraw.com/.well-known/mcp/*" [[env.staging.routes]] custom_domain = true pattern = "staging-sync.tldraw.xyz" [[env.production.routes]] zone_name = "tldraw.com" pattern = "www.tldraw.com/api/*" # See the staging comment above. [[env.production.routes]] zone_name = "tldraw.com" pattern = "www.tldraw.com/.well-known/oauth-protected-resource/*" [[env.production.routes]] zone_name = "tldraw.com" pattern = "www.tldraw.com/.well-known/oauth-protected-resource" [[env.production.routes]] zone_name = "tldraw.com" pattern = "www.tldraw.com/.well-known/mcp/*" [[env.production.routes]] custom_domain = true pattern = "sync.tldraw.xyz" #################### Durable objects #################### # durable objects have the same configuration in all environments: [env.dev.durable_objects] bindings = [ { name = "TLDR_DOC", class_name = "TLFileDurableObject" }, { name = "TL_FILE_EFFECTS", class_name = "TLFileEffectProcessor" }, { name = "TL_LOGGER", class_name = "TLLoggerDurableObject" }, ] [durable_objects] bindings = [ { name = "TLDR_DOC", class_name = "TLFileDurableObject" }, { name = "TL_FILE_EFFECTS", class_name = "TLFileEffectProcessor" }, { name = "TL_LOGGER", class_name = "TLLoggerDurableObject" }, ] [env.preview.durable_objects] bindings = [ { name = "TLDR_DOC", class_name = "TLFileDurableObject" }, { name = "TL_FILE_EFFECTS", class_name = "TLFileEffectProcessor" }, { name = "TL_LOGGER", class_name = "TLLoggerDurableObject" }, ] [env.staging.durable_objects] bindings = [ { name = "TLDR_DOC", class_name = "TLFileDurableObject" }, { name = "TL_FILE_EFFECTS", class_name = "TLFileEffectProcessor" }, { name = "TL_LOGGER", class_name = "TLLoggerDurableObject" }, ] [env.production.durable_objects] bindings = [ { name = "TLDR_DOC", class_name = "TLFileDurableObject" }, { name = "TL_FILE_EFFECTS", class_name = "TLFileEffectProcessor" }, { name = "TL_LOGGER", class_name = "TLLoggerDurableObject" }, ] #################### Analytics engine #################### # durable objects have the same configuration in all environments: [[env.dev.analytics_engine_datasets]] binding = "MEASURE" [[env.preview.analytics_engine_datasets]] binding = "MEASURE" [[env.staging.analytics_engine_datasets]] binding = "MEASURE" [[env.production.analytics_engine_datasets]] binding = "MEASURE" #################### Rooms R2 bucket #################### # in dev, we write to the preview bucket and need a `preview_bucket_name` [[env.dev.r2_buckets]] binding = "ROOMS" bucket_name = "rooms-preview" preview_bucket_name = "rooms-preview" # in preview and staging we write to the preview bucket [[env.preview.r2_buckets]] binding = "ROOMS" bucket_name = "rooms-preview" [[env.staging.r2_buckets]] binding = "ROOMS" bucket_name = "rooms-preview" # in production, we write to the main bucket [[env.production.r2_buckets]] binding = "ROOMS" bucket_name = "rooms" #################### Rooms History bucket #################### # in dev, we write to the preview bucket and need a `preview_bucket_name` [[env.dev.r2_buckets]] binding = "ROOMS_HISTORY_EPHEMERAL" bucket_name = "rooms-history-ephemeral-preview" preview_bucket_name = "rooms-history-ephemeral-preview" # in preview and staging we write to the preview bucket [[env.preview.r2_buckets]] binding = "ROOMS_HISTORY_EPHEMERAL" bucket_name = "rooms-history-ephemeral-preview" [[env.staging.r2_buckets]] binding = "ROOMS_HISTORY_EPHEMERAL" bucket_name = "rooms-history-ephemeral-preview" # in production, we write to the main bucket [[env.production.r2_buckets]] binding = "ROOMS_HISTORY_EPHEMERAL" bucket_name = "rooms-history-ephemeral" #################### Rooms History chain bucket #################### [[env.dev.r2_buckets]] binding = "ROOMS_HISTORY" bucket_name = "rooms-history-preview" preview_bucket_name = "rooms-history-preview" [[env.preview.r2_buckets]] binding = "ROOMS_HISTORY" bucket_name = "rooms-history-preview" [[env.staging.r2_buckets]] binding = "ROOMS_HISTORY" bucket_name = "rooms-history-preview" [[env.production.r2_buckets]] binding = "ROOMS_HISTORY" bucket_name = "rooms-history" #################### Room snapshots bucket #################### # in dev, we write to the preview bucket and need a `preview_bucket_name` [[env.dev.r2_buckets]] binding = "ROOM_SNAPSHOTS" bucket_name = "room-snapshots-preview" preview_bucket_name = "room-snapshots-preview" # in preview and staging we write to the preview bucket [[env.preview.r2_buckets]] binding = "ROOM_SNAPSHOTS" bucket_name = "room-snapshots-preview" [[env.staging.r2_buckets]] binding = "ROOM_SNAPSHOTS" bucket_name = "room-snapshots-preview" # in production, we write to the main bucket [[env.production.r2_buckets]] binding = "ROOM_SNAPSHOTS" bucket_name = "room-snapshots" #################### Assets R2 bucket #################### # in dev, we write to the preview bucket and need a `preview_bucket_name` [[env.dev.r2_buckets]] binding = 'UPLOADS' bucket_name = 'uploads-preview' preview_bucket_name = 'uploads-preview' # in preview and staging we write to the preview bucket [[env.preview.r2_buckets]] binding = 'UPLOADS' bucket_name = 'uploads-preview' [[env.staging.r2_buckets]] binding = 'UPLOADS' bucket_name = 'uploads-preview' # in production, we write to the main bucket [[env.production.r2_buckets]] binding = "UPLOADS" bucket_name = "uploads" #################### Thumbnails R2 bucket #################### # cache for board thumbnails / OG images (`og/…` keys) plus their pending-render markers and render # token records. No key carries a version, so nothing accumulates here, and the bucket has NO # expiration rule — the current thumbnail has to outlive any lifecycle window. See "Why two buckets" # in apps/dotcom/browser-run-thumbnails.md. # in dev, we write to the preview bucket and need a `preview_bucket_name` [[env.dev.r2_buckets]] binding = "THUMBNAILS" bucket_name = "thumbnails-preview" preview_bucket_name = "thumbnails-preview" # in preview and staging we write to the preview bucket [[env.preview.r2_buckets]] binding = "THUMBNAILS" bucket_name = "thumbnails-preview" [[env.staging.r2_buckets]] binding = "THUMBNAILS" bucket_name = "thumbnails-preview" # in production, we write to the main bucket [[env.production.r2_buckets]] binding = "THUMBNAILS" bucket_name = "thumbnails" #################### MCP data R2 bucket #################### # cache for MCP tool screenshots (`mcp/…` keys). Separate from THUMBNAILS because the retention is the # opposite: this key includes the board's content version, so every edit strands the previous object. # These buckets carry an expiration lifecycle rule (see the ops setup in browser-run-thumbnails.md). [[env.dev.r2_buckets]] binding = "MCP_DATA_BUCKET" bucket_name = "dotcom-mcp-data-preview" preview_bucket_name = "dotcom-mcp-data-preview" [[env.preview.r2_buckets]] binding = "MCP_DATA_BUCKET" bucket_name = "dotcom-mcp-data-preview" [[env.staging.r2_buckets]] binding = "MCP_DATA_BUCKET" bucket_name = "dotcom-mcp-data-preview" [[env.production.r2_buckets]] binding = "MCP_DATA_BUCKET" bucket_name = "dotcom-mcp-data" #################### Browser Rendering binding #################### # Drives the thumbnail render page through a Cloudflare-hosted headless browser (see # mcpServer.ts). Chrome runs in Cloudflare's Browser Rendering fleet, not in this # worker's isolate. The dev binding is intentionally NOT marked `remote`: a remote binding makes # `wrangler dev` require a CLOUDFLARE_API_TOKEN, which the credential-free process-compose e2e stack # does not have, so it would fail to boot. Local dev therefore gets a non-functional local BROWSER # binding (the render path fails closed); to exercise real Browser Run locally, run `wrangler dev # --remote` with Cloudflare credentials, or use a preview deploy. [env.dev.browser] binding = "BROWSER" [env.preview.browser] binding = "BROWSER" [env.staging.browser] binding = "BROWSER" [env.production.browser] binding = "BROWSER" #################### Room snapshots parent slug KV store #################### [[env.dev.kv_namespaces]] binding = "SNAPSHOT_SLUG_TO_PARENT_SLUG" id = "5eaa50a2b87145e582661ea3344804b8" [[env.preview.kv_namespaces]] binding = "SNAPSHOT_SLUG_TO_PARENT_SLUG" id = "5eaa50a2b87145e582661ea3344804b8" [[env.staging.kv_namespaces]] binding = "SNAPSHOT_SLUG_TO_PARENT_SLUG" id = "5eaa50a2b87145e582661ea3344804b8" [[env.production.kv_namespaces]] binding = "SNAPSHOT_SLUG_TO_PARENT_SLUG" id = "c6ce1f45447e4a44a00edb2a2077bc5c" #################### Key value storage #################### [[env.dev.kv_namespaces]] binding = "SLUG_TO_READONLY_SLUG" id = "847a6bded62045c6808dda6a275ef96c" [[env.dev.kv_namespaces]] binding = "READONLY_SLUG_TO_SLUG" id = "0a83acab40374ccd918cc9d755741714" [[env.preview.kv_namespaces]] binding = "SLUG_TO_READONLY_SLUG" id = "847a6bded62045c6808dda6a275ef96c" [[env.preview.kv_namespaces]] binding = "READONLY_SLUG_TO_SLUG" id = "0a83acab40374ccd918cc9d755741714" [[env.staging.kv_namespaces]] binding = "SLUG_TO_READONLY_SLUG" id = "847a6bded62045c6808dda6a275ef96c" [[env.staging.kv_namespaces]] binding = "READONLY_SLUG_TO_SLUG" id = "0a83acab40374ccd918cc9d755741714" [[env.production.kv_namespaces]] binding = "SLUG_TO_READONLY_SLUG" id = "2fb5fc7f7ca54a5a9dfae1b07a30a778" [[env.production.kv_namespaces]] binding = "READONLY_SLUG_TO_SLUG" id = "96be6637b281412ab35b2544539d78e8" #################### Feature flags KV store #################### # Dev and preview SHARE a namespace; staging and production each have their own. # Newly load-bearing: mcp_server_access is what decides who may drive the MCP server, so enabling the # flag locally enables it on every preview deploy at once. Give preview its own namespace before # anything here gates something that matters on a preview URL. [[env.dev.kv_namespaces]] binding = "FEATURE_FLAGS" id = "81a6f84741754ac4a9171f575f2aeece" [[env.preview.kv_namespaces]] binding = "FEATURE_FLAGS" id = "81a6f84741754ac4a9171f575f2aeece" [[env.staging.kv_namespaces]] binding = "FEATURE_FLAGS" id = "7b7422a178e641b19a846bfec912e399" # Production has its own namespace [[env.production.kv_namespaces]] binding = "FEATURE_FLAGS" id = "cf1dc78773ec48e5a44b184fd4734094" #################### Version metadata #################### [version_metadata] binding = "CF_VERSION_METADATA" [env.dev.version_metadata] binding = "CF_VERSION_METADATA" [env.preview.version_metadata] binding = "CF_VERSION_METADATA" [env.staging.version_metadata] binding = "CF_VERSION_METADATA" [env.production.version_metadata] binding = "CF_VERSION_METADATA" #################### Rate limiting #################### [[env.dev.unsafe.bindings]] name = "RATE_LIMITER" type = "ratelimit" namespace_id = "1001" simple = { limit = 600, period = 60 } [[env.preview.unsafe.bindings]] name = "RATE_LIMITER" type = "ratelimit" namespace_id = "1002" simple = { limit = 600, period = 60 } [[env.staging.unsafe.bindings]] name = "RATE_LIMITER" type = "ratelimit" namespace_id = "1003" simple = { limit = 600, period = 60 } [[env.production.unsafe.bindings]] name = "RATE_LIMITER" type = "ratelimit" namespace_id = "1004" simple = { limit = 600, period = 60 } # MCP server per-account request guard: one budget per signed-in caller, across every tool that can # spend Browser Run. It counts *calls*, not captures, and the clustering tools each run a measure # render, so ten a minute is nearer three finished screenshots a minute — see MCP_PER_USER_RATE_LIMIT # in config.ts before changing this number. # A binding carries one `limit` applied per key, so per-account and per-board need separate bindings to # hold different numbers. `period` must be 10 or 60. Keep these in step with the fallback constants in # mcpServer.ts, which are all that unit tests can see. # The name is the odd one out in this family: it keeps MCP_SCREENSHOT_ because it is deployed and # counting. It keyed on client IP until the endpoint required authentication; renaming live state buys # nothing, and the key it counts is now the user id. [[env.dev.unsafe.bindings]] name = "MCP_SCREENSHOT_RATE_LIMITER" type = "ratelimit" namespace_id = "1005" simple = { limit = 10, period = 60 } [[env.preview.unsafe.bindings]] name = "MCP_SCREENSHOT_RATE_LIMITER" type = "ratelimit" namespace_id = "1006" simple = { limit = 10, period = 60 } [[env.staging.unsafe.bindings]] name = "MCP_SCREENSHOT_RATE_LIMITER" type = "ratelimit" namespace_id = "1007" simple = { limit = 10, period = 60 } [[env.production.unsafe.bindings]] name = "MCP_SCREENSHOT_RATE_LIMITER" type = "ratelimit" namespace_id = "1008" simple = { limit = 10, period = 60 } # MCP server per-board capture guard, kept tighter than the per-account one so no single board can # absorb a caller's whole allowance. Captures only: measure renders are counted against the global # budget instead, because three of them land on one board in the ordinary cluster-screenshot flow. [[env.dev.unsafe.bindings]] name = "MCP_SERVER_BOARD_RATE_LIMITER" type = "ratelimit" namespace_id = "1013" simple = { limit = 2, period = 60 } [[env.preview.unsafe.bindings]] name = "MCP_SERVER_BOARD_RATE_LIMITER" type = "ratelimit" namespace_id = "1014" simple = { limit = 2, period = 60 } [[env.staging.unsafe.bindings]] name = "MCP_SERVER_BOARD_RATE_LIMITER" type = "ratelimit" namespace_id = "1015" simple = { limit = 2, period = 60 } [[env.production.unsafe.bindings]] name = "MCP_SERVER_BOARD_RATE_LIMITER" type = "ratelimit" namespace_id = "1016" simple = { limit = 2, period = 60 } # global cap on Browser Run invocations from the MCP server (keyed on a single shared key) [[env.dev.unsafe.bindings]] name = "MCP_SERVER_BROWSER_RATE_LIMITER" type = "ratelimit" namespace_id = "1009" simple = { limit = 20, period = 60 } [[env.preview.unsafe.bindings]] name = "MCP_SERVER_BROWSER_RATE_LIMITER" type = "ratelimit" namespace_id = "1010" simple = { limit = 20, period = 60 } [[env.staging.unsafe.bindings]] name = "MCP_SERVER_BROWSER_RATE_LIMITER" type = "ratelimit" namespace_id = "1011" simple = { limit = 20, period = 60 } [[env.production.unsafe.bindings]] name = "MCP_SERVER_BROWSER_RATE_LIMITER" type = "ratelimit" namespace_id = "1012" simple = { limit = 20, period = 60 } # MCP server per-account `search_boards` guard. Bounds Postgres rather than Browser Run, which search # never spends, so it is a separate budget from MCP_SCREENSHOT_RATE_LIMITER and a much larger number: # paging is what spends it, at a fixed 20 boards a page — see MCP_SEARCH_PER_USER_RATE_LIMIT in # config.ts before changing this. [[env.dev.unsafe.bindings]] name = "MCP_SERVER_SEARCH_RATE_LIMITER" type = "ratelimit" namespace_id = "1017" simple = { limit = 60, period = 60 } [[env.preview.unsafe.bindings]] name = "MCP_SERVER_SEARCH_RATE_LIMITER" type = "ratelimit" namespace_id = "1018" simple = { limit = 60, period = 60 } [[env.staging.unsafe.bindings]] name = "MCP_SERVER_SEARCH_RATE_LIMITER" type = "ratelimit" namespace_id = "1019" simple = { limit = 60, period = 60 } [[env.production.unsafe.bindings]] name = "MCP_SERVER_SEARCH_RATE_LIMITER" type = "ratelimit" namespace_id = "1020" simple = { limit = 60, period = 60 } #################### Queues #################### [[env.dev.queues.producers]] queue = "tldraw-multiplayer-queue-dev" binding = "QUEUE" [[env.dev.queues.consumers]] queue = "tldraw-multiplayer-queue-dev" max_retries = 10 [[env.staging.queues.producers]] queue = "tldraw-multiplayer-queue-staging" binding = "QUEUE" [[env.staging.queues.consumers]] queue = "tldraw-multiplayer-queue-staging" max_retries = 10 [[env.production.queues.producers]] queue = "tldraw-multiplayer-queue" binding = "QUEUE" [[env.production.queues.consumers]] queue = "tldraw-multiplayer-queue" max_retries = 10 dead_letter_queue = 'tldraw-multiplayer-dlq' # Static Assets per environment (see the top-level [assets] note — non-inheritable, so repeated). [env.dev.assets] directory = "assets" binding = "ASSETS" run_worker_first = true [env.preview.assets] directory = "assets" binding = "ASSETS" run_worker_first = true [env.staging.assets] directory = "assets" binding = "ASSETS" run_worker_first = true [env.production.assets] directory = "assets" binding = "ASSETS" run_worker_first = true