| 1 | // Diffinery: one Worker, everything on Cloudflare. |
| 2 | // `scripts/bootstrap.sh` creates the account resources and writes |
| 3 | // wrangler.deploy.jsonc (this file plus real IDs) for CI deploys. |
| 4 | { |
| 5 | "$schema": "node_modules/wrangler/config-schema.json", |
| 6 | "name": "diffinery", |
| 7 | "main": "src/index.ts", |
| 8 | "compatibility_date": "2026-10-01", |
| 9 | "compatibility_flags": ["nodejs_compat"], |
| 10 | "workers_dev": false, |
| 11 | // A zone route, not a Custom Domain: diffinery.com already has DNS records, and a |
| 12 | // route runs the Worker in front of them as long as the record is proxied (orange cloud). |
| 13 | "routes": [{ "pattern": "diffinery.com/*", "zone_name": "diffinery.com" }], |
| 14 | "observability": { "enabled": true, "head_sampling_rate": 1 }, |
| 15 | "upload_source_maps": true, |
| 16 | "limits": { "cpu_ms": 300000 }, |
| 17 | |
| 18 | "vars": { |
| 19 | "PUBLIC_URL": "https://diffinery.com", |
| 20 | "MAIL_FROM": "notifications@diffinery.com", |
| 21 | "BOOTSTRAP_ADMINS": "iletisim@canberkdemirci.com,canberkdemirci@icloud.com,iletisim@altugankarali.com", |
| 22 | "ACCESS_TEAM_DOMAIN": "", |
| 23 | "ACCESS_AUD": "" |
| 24 | }, |
| 25 | |
| 26 | "d1_databases": [ |
| 27 | { "binding": "DB", "database_name": "diffinery", "database_id": "00000000-0000-0000-0000-000000000000", "migrations_dir": "migrations" } |
| 28 | ], |
| 29 | "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "diffinery-data" }], |
| 30 | "queues": { |
| 31 | "producers": [ |
| 32 | { "binding": "EVENTS", "queue": "diffinery-events" }, |
| 33 | { "binding": "MAIL", "queue": "diffinery-mail" } |
| 34 | ], |
| 35 | "consumers": [ |
| 36 | // 20 events per batch: each makes D1 queries (webhooks, notifications, audit), and one consumer |
| 37 | // invocation may make at most 1,000. |
| 38 | { "queue": "diffinery-events", "max_batch_size": 20, "max_batch_timeout": 2, "max_retries": 8, "dead_letter_queue": "diffinery-events-dlq" }, |
| 39 | { "queue": "diffinery-mail", "max_batch_size": 20, "max_batch_timeout": 2, "max_retries": 8, "dead_letter_queue": "diffinery-mail-dlq" }, |
| 40 | // Bounces and complaints: subscribe this queue to Email Sending's events (docs/admin/site-administration.md). |
| 41 | { "queue": "diffinery-mail-events", "max_batch_size": 50, "max_batch_timeout": 5, "max_retries": 8 } |
| 42 | ] |
| 43 | }, |
| 44 | // Cloudflare Email Sending (notifications, security alerts, invites). The sending |
| 45 | // domain is onboarded under Compute > Email Service > Email Sending (see scripts/email-sending.sh). |
| 46 | "send_email": [{ "name": "EMAIL" }], |
| 47 | // Request rate limits per minute (src/lib/rate-limit.ts; RATE_LIMITS there must match). Local |
| 48 | // development declares none, so nothing is limited there. |
| 49 | "ratelimits": [ |
| 50 | { "name": "RL_AUTH", "namespace_id": "1001", "simple": { "limit": 20, "period": 60 } }, |
| 51 | { "name": "RL_ANON", "namespace_id": "1002", "simple": { "limit": 300, "period": 60 } }, |
| 52 | { "name": "RL_ACCOUNT", "namespace_id": "1003", "simple": { "limit": 2000, "period": 60 } }, |
| 53 | { "name": "RL_CREATE", "namespace_id": "1004", "simple": { "limit": 60, "period": 60 } }, |
| 54 | // Model requests through the proxy (/-/model/*), per paying person (agentic/proxy.ts MODEL_RATE_LIMIT). |
| 55 | { "name": "RL_MODEL", "namespace_id": "1005", "simple": { "limit": 600, "period": 60 } } |
| 56 | ], |
| 57 | "durable_objects": { |
| 58 | "bindings": [ |
| 59 | { "name": "REPO", "class_name": "RepoDO" }, |
| 60 | { "name": "SCHEDULER", "class_name": "SchedulerDO" }, |
| 61 | { "name": "RUNNER", "class_name": "RunnerContainer" }, |
| 62 | // Smaller hosted runners (src/runner/sizes.ts), the same runner on smaller instance types. |
| 63 | { "name": "RUNNER_MEDIUM", "class_name": "RunnerContainerMedium" }, |
| 64 | { "name": "RUNNER_SMALL", "class_name": "RunnerContainerSmall" }, |
| 65 | { "name": "RUNNER_TINY", "class_name": "RunnerContainerTiny" }, |
| 66 | { "name": "SANDBOX", "class_name": "SandboxContainer" }, |
| 67 | { "name": "IMPORTER", "class_name": "ImportContainer" }, |
| 68 | { "name": "PROJECT", "class_name": "ProjectDO" } |
| 69 | ] |
| 70 | }, |
| 71 | "migrations": [ |
| 72 | { "tag": "v1", "new_sqlite_classes": ["RepoDO", "RunnerContainer", "SandboxContainer"] }, |
| 73 | { "tag": "v2", "new_sqlite_classes": ["ImportContainer"] }, |
| 74 | // Agent tasks, one Durable Object per repository (agentic/ProjectDO.ts). |
| 75 | { "tag": "v3", "new_sqlite_classes": ["ProjectDO"] }, |
| 76 | // Workflow schedules (scheduler.ts). |
| 77 | { "tag": "v4", "new_sqlite_classes": ["SchedulerDO"] }, |
| 78 | // Smaller hosted runner sizes (src/runner/sizes.ts). |
| 79 | { "tag": "v5", "new_sqlite_classes": ["RunnerContainerMedium", "RunnerContainerSmall", "RunnerContainerTiny"] } |
| 80 | ], |
| 81 | "workflows": [ |
| 82 | { "name": "diffinery-run", "binding": "RUN_WORKFLOW", "class_name": "RunWorkflow" }, |
| 83 | { "name": "diffinery-import", "binding": "IMPORT_WORKFLOW", "class_name": "ImportWorkflow" }, |
| 84 | // Account exports and deletions (src/accounts/workflow.ts). |
| 85 | { "name": "diffinery-account", "binding": "ACCOUNT_WORKFLOW", "class_name": "AccountWorkflow" } |
| 86 | ], |
| 87 | // A deploy that changes the image rolls the running containers: once a container has been up |
| 88 | // longer than rollout_active_grace_period seconds, it gets SIGTERM and is killed 15 minutes later. |
| 89 | // Runner and import containers are covered for their whole life (a job is capped at 6 hours, an |
| 90 | // import transfer too), so a deploy shouldn't cut one short; they pick up the new image next start. |
| 91 | // The platform doesn't guarantee it: a job that started on an old-image container during a rollout |
| 92 | // was replaced minutes in. So a runner on a replaced image is turned away before it runs anything |
| 93 | // (repo/actions.ts STALE_IMAGE_TRIES), and a job whose runner is replaced before its steps ran |
| 94 | // starts again on a new one (MAX_RUNNER_RESTARTS). |
| 95 | // The rollout is one step (rollout_step_percentage 100): with the default [10, 100], the first |
| 96 | // step still starts 90% of new containers on the old image, and the next step replaced such a |
| 97 | // job's container seconds after it started. The rollout's SIGTERM reaches every process in the |
| 98 | // container, so the job's steps died with it. |
| 99 | "containers": [ |
| 100 | // standard-4 (4 vCPU, 12 GiB, 20 GB): standard-2 has 1 vCPU, on which typecheck and test steps took |
| 101 | // 2.3 to 2.6 times as long. Active CPU is what's billed, so a faster job costs about the same. |
| 102 | { "class_name": "RunnerContainer", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-4", "max_instances": 100, "rollout_active_grace_period": 23400, "rollout_step_percentage": 100 }, |
| 103 | // Smaller sizes for jobs that use little of a large runner (src/runner/sizes.ts): Cloudflare bills a container's |
| 104 | // memory and disk as provisioned, used or not, so a job that waits on the network costs a large runner's memory. |
| 105 | // A job gets one when runs-on asks for it, or when its own earlier runs show it fits with room to spare. |
| 106 | { "class_name": "RunnerContainerMedium", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-3", "max_instances": 100, "rollout_active_grace_period": 23400, "rollout_step_percentage": 100 }, |
| 107 | { "class_name": "RunnerContainerSmall", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-1", "max_instances": 100, "rollout_active_grace_period": 23400, "rollout_step_percentage": 100 }, |
| 108 | { "class_name": "RunnerContainerTiny", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "basic", "max_instances": 100, "rollout_active_grace_period": 23400, "rollout_step_percentage": 100 }, |
| 109 | { "class_name": "SandboxContainer", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-1", "max_instances": 50, "rollout_active_grace_period": 3600, "rollout_step_percentage": 100 }, |
| 110 | // GitHub imports: a mirror clone of a large repository needs disk and memory. |
| 111 | { "class_name": "ImportContainer", "image": "../../containers/runner/Dockerfile", "image_build_context": "../../containers/runner", "instance_type": "standard-4", "max_instances": 20, "rollout_active_grace_period": 25200, "rollout_step_percentage": 100 } |
| 112 | ], |
| 113 | // Housekeeping, and a check that the workflow scheduler's alarm is set (src/scheduler.ts). |
| 114 | "triggers": { "crons": ["*/5 * * * *"] }, |
| 115 | |
| 116 | "env": { |
| 117 | // Local development and end-to-end tests: no Access, a fixed dev identity, no containers. |
| 118 | "dev": { |
| 119 | "routes": [], |
| 120 | "workers_dev": true, |
| 121 | "vars": { |
| 122 | "PUBLIC_URL": "http://localhost:8787", |
| 123 | "MAIL_FROM": "notifications@diffinery.com", |
| 124 | "BOOTSTRAP_ADMINS": "dev@diffinery.local", |
| 125 | "ACCESS_TEAM_DOMAIN": "", |
| 126 | "ACCESS_AUD": "", |
| 127 | "DEV_AUTH": "dev@diffinery.local", |
| 128 | // Containers do not run under wrangler dev: an import agent started by hand (or by the e2e test) claims Git transfers. |
| 129 | "IMPORT_GIT_AGENT": "external" |
| 130 | }, |
| 131 | "d1_databases": [{ "binding": "DB", "database_name": "diffinery-dev", "database_id": "00000000-0000-0000-0000-000000000000", "migrations_dir": "migrations" }], |
| 132 | "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "diffinery-data-dev" }], |
| 133 | "queues": { |
| 134 | "producers": [ |
| 135 | { "binding": "EVENTS", "queue": "diffinery-events-dev" }, |
| 136 | { "binding": "MAIL", "queue": "diffinery-mail-dev" } |
| 137 | ], |
| 138 | "consumers": [ |
| 139 | { "queue": "diffinery-events-dev", "max_batch_size": 50, "max_batch_timeout": 1 }, |
| 140 | { "queue": "diffinery-mail-dev", "max_batch_size": 20, "max_batch_timeout": 1 }, |
| 141 | { "queue": "diffinery-mail-events-dev", "max_batch_size": 50, "max_batch_timeout": 1 } |
| 142 | ] |
| 143 | }, |
| 144 | // Local email: Miniflare captures sent messages instead of delivering them. |
| 145 | "send_email": [{ "name": "EMAIL" }], |
| 146 | "durable_objects": { |
| 147 | "bindings": [ |
| 148 | { "name": "REPO", "class_name": "RepoDO" }, |
| 149 | { "name": "SCHEDULER", "class_name": "SchedulerDO" }, |
| 150 | { "name": "RUNNER", "class_name": "RunnerContainer" }, |
| 151 | { "name": "RUNNER_MEDIUM", "class_name": "RunnerContainerMedium" }, |
| 152 | { "name": "RUNNER_SMALL", "class_name": "RunnerContainerSmall" }, |
| 153 | { "name": "RUNNER_TINY", "class_name": "RunnerContainerTiny" }, |
| 154 | // Sandboxes run in an `agent.mjs --sandbox-pool` process (SANDBOX_POOL_URL), not containers. |
| 155 | { "name": "SANDBOX", "class_name": "SandboxPoolDO" }, |
| 156 | { "name": "IMPORTER", "class_name": "ImportContainer" }, |
| 157 | { "name": "PROJECT", "class_name": "ProjectDO" } |
| 158 | ] |
| 159 | }, |
| 160 | "workflows": [ |
| 161 | { "name": "diffinery-run-dev", "binding": "RUN_WORKFLOW", "class_name": "RunWorkflow" }, |
| 162 | { "name": "diffinery-import-dev", "binding": "IMPORT_WORKFLOW", "class_name": "ImportWorkflow" }, |
| 163 | { "name": "diffinery-account-dev", "binding": "ACCOUNT_WORKFLOW", "class_name": "AccountWorkflow" } |
| 164 | ], |
| 165 | "triggers": { "crons": [] }, |
| 166 | "containers": [] |
| 167 | } |
| 168 | } |
| 169 | } |