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
732 lines
24 KiB
TOML
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
|