Files
upstream-tldraw/apps/dotcom/sync-worker/wrangler.toml
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

732 lines
24 KiB
TOML

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