| 1 | --- |
| 2 | title: Deploy g1t to Cloudflare |
| 3 | description: How g1t.sh is deployed to Cloudflare, and how to deploy the same code to an account of your own. |
| 4 | --- |
| 5 | |
| 6 | g1t.sh runs on Cloudflare, and the tool that deploys it is in the |
| 7 | repository. You can deploy the same code to a Cloudflare account of your |
| 8 | own. To run g1t on one machine without Cloudflare, see |
| 9 | [Run g1t yourself](/guides/self-hosting/) instead. |
| 10 | |
| 11 | You need a Cloudflare account on the Workers Paid plan (Cloudflare |
| 12 | Artifacts, where repositories are kept, requires it), Node 24, |
| 13 | Rust with the `wasm32-unknown-unknown` target, and Docker to build the |
| 14 | runner's image. |
| 15 | |
| 16 | Every `wrangler.jsonc` names g1t.sh's account, domains and resource ids. |
| 17 | Before a first deploy to another account, change those to your own (see |
| 18 | [A first deploy to a new account](#a-first-deploy-to-a-new-account)). |
| 19 | |
| 20 | ## The pieces |
| 21 | |
| 22 | | Piece | Where | |
| 23 | | --- | --- | |
| 24 | | The manifest: every deployable part | `deploy/stack.jsonc` | |
| 25 | | The tool | `scripts/deploy.mjs`, with its library and tests in `scripts/deploy/` | |
| 26 | | Rust Worker builds | `scripts/build-rust-worker.mjs` | |
| 27 | | The runner's images | `services/runner/base/Dockerfile`, `services/runner/Dockerfile`, `services/runner/base.json` | |
| 28 | | The workflows | `.g1t/workflows/deploy.yml`, `.g1t/workflows/runner-base.yml` | |
| 29 | |
| 30 | ## The manifest |
| 31 | |
| 32 | `deploy/stack.jsonc` lists every unit: each folder with a `wrangler.jsonc`. |
| 33 | |
| 34 | | Field | What it is | |
| 35 | | --- | --- | |
| 36 | | `path` | The unit's folder | |
| 37 | | `kind` | `rust-worker`, `ts-worker`, `react-router` or `astro` | |
| 38 | | `worker` | The Worker's name. It must match its `wrangler.jsonc`. | |
| 39 | | `d1` | `{ database, migrations }`, when it has a database | |
| 40 | | `stage` | `core`, `edge` or `front` (below) | |
| 41 | | `secrets` | The Wrangler secrets it needs, by name | |
| 42 | | `setup` | One-time steps a first deploy needs that no config can say | |
| 43 | | `inputs` | Files outside its folder it is built from that no package names | |
| 44 | | `image` | A Containers image, for the runner | |
| 45 | | `self_host` | `run`, `off`, `separate` or `none`: what a [self-hosted installation](/guides/self-hosting-architecture/) does with it | |
| 46 | |
| 47 | What a unit is built from is read, never listed by hand: Rust path |
| 48 | dependencies from `cargo metadata`, workspace packages from each |
| 49 | `package.json`. A lockfile change counts for a unit only when a package |
| 50 | it uses changed. `node scripts/deploy.mjs manifest` prints the result. |
| 51 | |
| 52 | Units deploy in stages. A stage starts only when the one before it |
| 53 | succeeded, and units inside a stage deploy in parallel: |
| 54 | |
| 55 | | Stage | What | |
| 56 | | --- | --- | |
| 57 | | `migrations` | Every pending D1 migration, before any code | |
| 58 | | `core` | The services, reached through bindings | |
| 59 | | `edge` | Public endpoints other than the site: `api`, `pages`, `status` | |
| 60 | | `front` | The site, sudo and the docs | |
| 61 | |
| 62 | A unit binds only to units in its own stage or an earlier one, so new code |
| 63 | never calls a service that has not shipped. `npm run test:deploy` checks |
| 64 | this, and that every `wrangler.jsonc` has a unit whose names match it. |
| 65 | Cloudflare refuses a Worker bound to a Worker that does not exist, so |
| 66 | inside a stage a unit whose Worker has never been deployed (a new or |
| 67 | renamed one) goes first, and the units of its stage that bind to it go |
| 68 | after it succeeds. |
| 69 | |
| 70 | ## The tool |
| 71 | |
| 72 | ```sh |
| 73 | node scripts/deploy.mjs plan # what would deploy, and why (read-only) |
| 74 | node scripts/deploy.mjs deploy # migrations, then every changed unit |
| 75 | node scripts/deploy.mjs deploy --only web,api # just these, if they changed |
| 76 | node scripts/deploy.mjs deploy --only web --force # just this, changed or not |
| 77 | node scripts/deploy.mjs deploy --all # everything |
| 78 | node scripts/deploy.mjs build --only events # build as a deploy would; upload nothing |
| 79 | node scripts/deploy.mjs migrate # pending migrations only |
| 80 | node scripts/deploy.mjs manifest --check # the manifest's problems |
| 81 | node scripts/deploy.mjs doctor # secrets each unit lacks |
| 82 | node scripts/deploy.mjs build-base # build and push the runner's base image (Docker) |
| 83 | node scripts/deploy.mjs image # make the runner's image in the registry (no Docker) |
| 84 | ``` |
| 85 | |
| 86 | | Flag | What it does | |
| 87 | | --- | --- | |
| 88 | | `--only a,b`, `--skip a,b` | Units by short name, folder or Worker name | |
| 89 | | `--all` | Every unit, changed or not | |
| 90 | | `--force` | The selected units even if unchanged | |
| 91 | | `--concurrency N` | Units at once inside a stage, and migrations at once (default 4) | |
| 92 | | `--stage core` | One stage only | |
| 93 | | `--no-migrations` | Skip the migrations step | |
| 94 | | `--allow-dirty` | Deploy with uncommitted changes. The version records no commit, so the next plan deploys it again. | |
| 95 | | `--rebuild-image`, `--rebuild-base` | Build the runner's image, or its base, even if one exists | |
| 96 | | `--rollback` | Allow deploying a commit older than the one live | |
| 97 | | `--since REV` | Treat Workers with no recorded commit as running `REV` | |
| 98 | |
| 99 | **Only what changed deploys.** Each deploy records its commit on the |
| 100 | Worker's version (`wrangler deploy --message "g1t-deploy <sha> ..." --tag |
| 101 | g1t-<sha>`). `plan` reads the live version of each Worker, diffs from its |
| 102 | commit to `HEAD`, and deploys the units the change touches. A change only |
| 103 | to a crate's tests deploys nothing. A version deployed any other way has |
| 104 | no commit, so the unit is deployed again. |
| 105 | |
| 106 | With `CLOUDFLARE_API_TOKEN` (in CI) or `CLOUDFLARE_DEPLOY_TOKEN` (on your |
| 107 | machine) and `CLOUDFLARE_ACCOUNT_ID`, `plan` reads Cloudflare's API itself |
| 108 | and takes a few seconds. With only `wrangler login`, it asks Wrangler and |
| 109 | takes minutes. Each unit's output is kept in `$TMPDIR/g1t-deploy/<unit>.log`. |
| 110 | |
| 111 | ### Migrations run while the old code is live |
| 112 | |
| 113 | Migrations apply before any code, so for a minute or more the old code |
| 114 | reads the new schema. Keep it working: |
| 115 | |
| 116 | - **Add, never change meaning.** New tables and columns with defaults are |
| 117 | safe. |
| 118 | - **Change meaning in two deploys.** First ship code that reads both forms; |
| 119 | then the migration that rewrites the rows. |
| 120 | - **Never drop or rename** a column or table in the same deploy that stops |
| 121 | reading it. |
| 122 | |
| 123 | Migrations never run backwards: undoing one is a new migration. |
| 124 | |
| 125 | ## The workflow |
| 126 | |
| 127 | `.g1t/workflows/deploy.yml` runs the tool on every push to `main`, and by |
| 128 | hand (**Actions → Deploy → Run workflow**) with `units`, `all` and |
| 129 | `dry_run`. |
| 130 | |
| 131 | | Job | Does | |
| 132 | | --- | --- | |
| 133 | | `check` | `manifest --check` and `npm run test:deploy` | |
| 134 | | `plan` | The plan, shown in the run's summary | |
| 135 | | `migrate` | Pending migrations; skipped when there are none | |
| 136 | | `core`, `edge`, `front` | Each stage's units, one job per build group | |
| 137 | | `smoke` | Loads the landing page, sign-in, sign-up and pricing | |
| 138 | |
| 139 | Deploys run one at a time; a second push waits. Rust units build on |
| 140 | `g1t-4core` machines, at most four to a job, with Cargo's target and |
| 141 | sccache kept in the Actions cache. |
| 142 | |
| 143 | The deploy jobs use `environment: production`, so only they can read the |
| 144 | Cloudflare token. Jobs on g1t's machines reach Cloudflare's API and |
| 145 | registry as they reach any host; there is nothing to allow. |
| 146 | |
| 147 | Every deploy shows on the repository's Deployments page. A deploy run by |
| 148 | hand reports one too when `G1T_DEPLOY_TOKEN` holds a g1t token with |
| 149 | `deployments:write`. |
| 150 | |
| 151 | ### The Cloudflare API token |
| 152 | |
| 153 | Make a custom token at **dash.cloudflare.com → My Profile → API Tokens**, |
| 154 | restricted to your account and zones: |
| 155 | |
| 156 | | Scope | Permission | |
| 157 | | --- | --- | |
| 158 | | Account | Workers Scripts: Edit | |
| 159 | | Account | D1: Edit | |
| 160 | | Account | Queues: Edit | |
| 161 | | Account | Workers R2 Storage: Read | |
| 162 | | Account | Account Settings: Read | |
| 163 | | Account | Containers: Edit | |
| 164 | | Zone | Workers Routes: Edit | |
| 165 | | Zone | DNS: Edit | |
| 166 | | Zone | Zone: Read | |
| 167 | |
| 168 | Store it on the repository under **Settings → Secrets and variables** as |
| 169 | the secret `CLOUDFLARE_API_TOKEN`, for workflows in the `production` |
| 170 | environment, and your account id as the variable `CLOUDFLARE_ACCOUNT_ID`. |
| 171 | A missing permission fails with `Authentication error [code: 10000]` and |
| 172 | the route it was refused on. |
| 173 | |
| 174 | ## A first deploy to a new account |
| 175 | |
| 176 | What the configs refer to must exist first. |
| 177 | `node scripts/deploy.mjs manifest --json` lists, for each unit, its |
| 178 | queues, KV namespaces, R2 buckets, Vectorize indexes and dispatch |
| 179 | namespaces; its `setup` and `secrets` say the rest. |
| 180 | |
| 181 | 1. Set `account_id`, routes and custom domains in each `wrangler.jsonc` to |
| 182 | your own. |
| 183 | 2. D1: `npx wrangler d1 create <database>` for each, and put its id in the |
| 184 | unit's `wrangler.jsonc`. |
| 185 | 3. KV: `npx wrangler kv namespace create <name>` for each name under |
| 186 | `resources.kv` in the manifest, and put the ids in the configs. |
| 187 | 4. Queues: `npx wrangler queues create <queue>` for each queue in the |
| 188 | manifest, including the dead-letter queue `g1t-events-dlq`. |
| 189 | 5. R2: create `g1t-screenshots`, `g1t-actions-cache` and `g1t-git-packs`, |
| 190 | with lifecycle rules: |
| 191 | |
| 192 | ```sh |
| 193 | npx wrangler r2 bucket lifecycle add g1t-actions-cache expire-cache c/ --expire-days 30 --abort-multipart-days 1 |
| 194 | npx wrangler r2 bucket lifecycle add g1t-actions-cache expire-artifacts a/ --expire-days 91 --abort-multipart-days 1 |
| 195 | npx wrangler r2 bucket lifecycle add g1t-git-packs expire-packs packs/ --expire-days 7 --abort-multipart-days 1 |
| 196 | ``` |
| 197 | |
| 198 | 6. The runner's base image: `node scripts/deploy.mjs build-base`, then |
| 199 | commit `services/runner/base.json`. |
| 200 | 7. Everything else each unit's `setup` names: Vectorize, the dispatch |
| 201 | namespace, DNS, Access, Email Sending and Artifacts. |
| 202 | 8. Secrets: `npx wrangler secret put <NAME>` in the unit's folder. |
| 203 | `node scripts/deploy.mjs doctor` lists what is missing. |
| 204 | 9. For [Deployments](/guides/deployments/), which need the Workers for |
| 205 | Platforms add-on and a zone for apps: `scripts/setup-deployments.sh`. |
| 206 | 10. Durable Objects: the artifacts service's v3 migration |
| 207 | (`services/artifacts/wrangler.jsonc`) moves g1t.sh's live rooms from |
| 208 | the Worker it was renamed from, which a new account does not have. |
| 209 | Replace it with |
| 210 | `{ "tag": "v3", "new_sqlite_classes": ["PageRoom", "FolioRoom"] }`. |
| 211 | 11. `node scripts/deploy.mjs deploy --all`. Inside a stage, Workers that do |
| 212 | not exist yet go before those bound to them; a Worker bound to one in a |
| 213 | later stage, or to one that failed, is refused until it exists. |
| 214 | 12. Register on your site to make the first account, or run |
| 215 | `node services/identity/scripts/create-user.mjs <username>`. |
| 216 | |
| 217 | ### Adding a unit |
| 218 | |
| 219 | 1. Make its folder with a `wrangler.jsonc`. A Rust Worker is a member of |
| 220 | the root `Cargo.toml`, with `node ../../scripts/build-rust-worker.mjs` |
| 221 | as its build command and `wasm-opt = ["-O1"]`; a TypeScript one is an |
| 222 | npm workspace. |
| 223 | 2. Add it to `deploy/stack.jsonc` in the earliest stage after everything |
| 224 | it binds to, with its secrets, setup and `self_host`. |
| 225 | 3. Add a row for it to the table in |
| 226 | [How a self-hosted g1t runs](/guides/self-hosting-architecture/#each-part). |
| 227 | 4. `npm run test:deploy` and `node scripts/deploy.mjs manifest --check` |
| 228 | say what is missing. |
| 229 | |
| 230 | ### Renaming a Worker |
| 231 | |
| 232 | A Worker's name is its identity on Cloudflare, so a renamed Worker is a |
| 233 | new one. Keep its resources: D1 databases, R2 buckets, Vectorize indexes |
| 234 | and queues are bound by id or name and keep theirs. Durable Objects |
| 235 | belong to a Worker, so the new one takes them with a |
| 236 | [transfer migration](https://developers.cloudflare.com/durable-objects/reference/durable-object-class-migrations-legacy/#transfer-migration) |
| 237 | (`transferred_classes`, with `from_script` the old name), every object's |
| 238 | storage with them; until the old Worker is deleted, its own bindings to |
| 239 | them reach the new one. A queue has one consumer, so the old Worker lets |
| 240 | go of it first. |
| 241 | |
| 242 | The artifacts service was `g1t-docs-service` (`services/docs`) and is |
| 243 | `g1t-artifacts` (`services/artifacts`). An installation that ran the old |
| 244 | one moves once, in this order: |
| 245 | |
| 246 | 1. Let go of the queue. Its messages wait for the new consumer: |
| 247 | |
| 248 | ```sh |
| 249 | npx wrangler queues consumer remove g1t-events-docs g1t-docs-service |
| 250 | ``` |
| 251 | |
| 252 | 2. Deploy: push to `main`, or `node scripts/deploy.mjs deploy`. |
| 253 | `g1t-artifacts` goes first in `core`, takes `PageRoom` and `FolioRoom` |
| 254 | and the queue; then `api` (edge) and `web` (front), each bound to it. |
| 255 | Until each has gone out it still calls |
| 256 | `g1t-docs-service`, which answers from the same database, bucket and |
| 257 | (forwarded) rooms. |
| 258 | 3. Check: an artifact opens with its content and edits live, a file |
| 259 | uploads, and `npx wrangler queues info g1t-events-docs` names |
| 260 | `g1t-artifacts` as the consumer. Cloudflare's |
| 261 | `GET /accounts/{account_id}/workers/durable_objects/namespaces` lists |
| 262 | `PageRoom` and `FolioRoom` under `g1t-artifacts`. |
| 263 | 4. After a day with no requests to it (`npx wrangler tail g1t-docs-service` |
| 264 | shows only its 03:17 UTC cron), delete it by hand: |
| 265 | `npx wrangler delete g1t-docs-service`. Never with `--force`: a refusal |
| 266 | means something still binds to it. |
| 267 | |
| 268 | If step 2 fails before `g1t-artifacts` deploys, nothing moved: put the |
| 269 | consumer back with `npx wrangler deploy` in `services/docs` at the |
| 270 | previous commit. Once it has deployed, the rooms are its own: fix forward. |
| 271 | |
| 272 | ## The runner's images |
| 273 | |
| 274 | `services/runner` runs every sandbox (checks, the merge queue, |
| 275 | workflow jobs and deploy builds) from one Containers image in two parts: |
| 276 | |
| 277 | | Image | Built from | Rebuilt | |
| 278 | | --- | --- | --- | |
| 279 | | The base | `services/runner/base/Dockerfile`: Debian, Node, Python, Docker, Go, Rust, Java, .NET, Ruby and the Claude Code CLI | When its folder changes, weekly by `runner-base.yml`, or by hand with `build-base` | |
| 280 | | The runner | `services/runner/Dockerfile`: the base plus the `g1t-runner` binary | When the binary or the base changes | |
| 281 | |
| 282 | `services/runner/base.json` records the base that was pushed; |
| 283 | `npm run test:deploy` fails while it and the base's folder disagree. The |
| 284 | runner's image is tagged with a hash of what it is built from, so a deploy |
| 285 | makes it only when the registry does not already have it. It is made in |
| 286 | the registry, without Docker: the deploy reads the base's manifest, |
| 287 | uploads one layer holding the binary, and writes a manifest that names the |
| 288 | base's layers and that one, with the entrypoint `services/runner/Dockerfile` |
| 289 | gives it. Nothing of the base is downloaded, so the deploy job needs no |
| 290 | room for it. If that fails and Docker runs where the deploy does, the |
| 291 | image is built from `services/runner/Dockerfile` and pushed instead, and |
| 292 | the log shows how full the disk was before and after. A dry run |
| 293 | (`build`, or `image --no-push`) builds it with Docker. When nothing |
| 294 | the image is built from changed, the deploy leaves running sandboxes |
| 295 | alone. When it did, Cloudflare replaces sandboxes on the new image but |
| 296 | leaves any that started within the last two hours |
| 297 | (`rollout_active_grace_period` in `services/runner/wrangler.jsonc`) to |
| 298 | finish first. A workflow job on g1t's machines runs for at most 60 |
| 299 | minutes, so a deploy never cuts one short; an agent run whose guardrails |
| 300 | allow more than two hours can be. |
| 301 | |
| 302 | The standard sandbox is the instance type `standard-3` (2 vCPU, 8 GiB, |
| 303 | 16 GB), and so is `runs-on: g1t-2core`, in a class of its own; jobs that |
| 304 | ask for `runs-on: g1t-4core` get `standard-4` (4 vCPU, 12 GiB, 20 GB). At |
| 305 | the `max_instances` in `services/runner/wrangler.jsonc` (20, 10 and 10), |
| 306 | 40 sandboxes at once hold 100 vCPU, 360 GiB of memory and 680 GB of disk, |
| 307 | which your account's Containers limits for live instances must allow. |
| 308 | |
| 309 | Set the runner Worker's `DOCKER` variable to `off` to give workflow jobs no |
| 310 | Docker Engine. |
| 311 | |
| 312 | `CLOUDFLARE_API_TOKEN=<token> node scripts/ops/runner-errors.mjs` sorts the |
| 313 | sandboxes' errors over the last 7 days into expected ones (a deploy |
| 314 | resetting them, no free capacity, a container that stopped) and ones to |
| 315 | read. |
| 316 | |
| 317 | ## Docker deployments |
| 318 | |
| 319 | Container apps ([Docker deployments](/guides/deployments/#docker-deployments)) |
| 320 | are off until `CONTAINERS` is `on` in `services/deployments/wrangler.jsonc`. |
| 321 | The code follows Cloudflare's documentation and Wrangler's own client for |
| 322 | these APIs, but none of them has been called against g1t's account yet. |
| 323 | Before turning it on, check each of these with a test project: |
| 324 | |
| 325 | 1. **The token.** `CLOUDFLARE_API_TOKEN` of `services/deployments` has |
| 326 | **Containers: Edit** (and the account has Containers, on Workers Paid). |
| 327 | 2. **Registry logins.** |
| 328 | `POST /accounts/{id}/containers/registries/registry.cloudflare.com/credentials` |
| 329 | with `{ "permissions": ["push"], "expiration_minutes": 15 }` answers |
| 330 | inside the API's usual `{ success, result }` with a `username` and |
| 331 | `password`, and `docker push` succeeds with that login alone, with no |
| 332 | `pull` (Docker checks for layers before it uploads them). If it needs |
| 333 | `pull`, do not add it: a pull login reads every app's image. Find |
| 334 | another way first, such as Cloudflare scoping logins to a repository. |
| 335 | 3. **Checking a digest.** g1t asks |
| 336 | `HEAD https://registry.cloudflare.com/v2/<account>/g1t-<app>/manifests/sha256:…` |
| 337 | with a pull login of its own, following a `WWW-Authenticate: Bearer` |
| 338 | challenge. It answers 200 for an image that was pushed. |
| 339 | 4. **The app's Worker.** A script uploaded to the dispatch namespace with |
| 340 | `exports: { "G1tApp": { "type": "durable-object", "storage": "sqlite" } }` |
| 341 | and `containers: [{ "class_name": "G1tApp" }]` in its metadata is taken, |
| 342 | and taken again on the next deploy with nothing changed about the class. |
| 343 | 5. **Its Durable Object namespace.** |
| 344 | `GET /accounts/{id}/workers/durable_objects/namespaces` lists it with |
| 345 | `class` `G1tApp`, `script` the app's name and `dispatch_namespace` |
| 346 | `g1t-deployments`. |
| 347 | 6. **The Container application.** `POST /accounts/{id}/containers/applications` |
| 348 | with `scheduling_policy` `default`, the image by digest, `instance_type`, |
| 349 | `max_instances` 1 and `durable_objects.namespace_id` creates it; a |
| 350 | rollout (`…/rollouts`, `strategy` `rolling`, `step_percentage` 100, |
| 351 | `kind` `full_auto`) moves it to a new image; `DELETE` removes it. |
| 352 | 7. **Starting.** `ctx.container.start({ env, enableInternet: true })` |
| 353 | works under the default policy from a Worker in a dispatch namespace, |
| 354 | the container gets its environment, and `getTcpPort(port).fetch()` |
| 355 | reaches it; the dispatcher's limits (50 ms of CPU, 50 subrequests) leave |
| 356 | room for a page that waits on a container starting. |
| 357 | 8. **Sleeping and billing.** Five minutes after the last request the |
| 358 | container stops (the app's object stops it itself), Cloudflare's billable |
| 359 | usage for Containers stops growing, and `container_meters` in the |
| 360 | deployments database has the minutes the app's Worker reported, which |
| 361 | should be at least what Cloudflare billed for the same time. |
| 362 | 9. **Pausing and taking down.** A workspace over its limit gets its |
| 363 | container app's Worker put back paused, which stops the container, and |
| 364 | taking an app down deletes its application before its Worker. |
| 365 | |
| 366 | Then set `CONTAINERS` to `on`, deploy `services/deployments`, and the |
| 367 | container build of a project with a `Dockerfile` goes live; the |
| 368 | [limitations page](/about/limitations/#containers-are-not-switched-on-yet) |
| 369 | and the caution in the deployments guide come out in the same change. |
| 370 | |
| 371 | ## Rolling back |
| 372 | |
| 373 | - **One unit, at once:** `npx wrangler rollback` in its folder. Revert the |
| 374 | commit on `main` too, or the next push brings it back. |
| 375 | - **To a commit:** check it out and run |
| 376 | `node scripts/deploy.mjs deploy --only <units> --rollback`. Without |
| 377 | `--rollback` the tool refuses a unit whose live commit is newer. |
| 378 | - **The runner's image:** redeploy the older commit with |
| 379 | `--only runner --rollback`; its image is still in the registry. |
| 380 | |
| 381 | ## Other one-time setup |
| 382 | |
| 383 | ### The dead-letter queue |
| 384 | |
| 385 | Every queue consumer retries a message a fixed number of times, then puts |
| 386 | it on `g1t-events-dlq`. Nothing consumes that queue: read it with |
| 387 | `npx wrangler queues info g1t-events-dlq`, fix the cause, and replay by |
| 388 | hand. |
| 389 | |
| 390 | ### OIDC tokens for workflow jobs |
| 391 | |
| 392 | The API issues workflow jobs' [OIDC tokens](/guides/actions/), signed with |
| 393 | an RSA key in its secret `ACTIONS_OIDC_KEY`. Without it, jobs get no |
| 394 | tokens. |
| 395 | |
| 396 | ```sh |
| 397 | openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out oidc.pem |
| 398 | cd apps/api && npx wrangler secret put ACTIONS_OIDC_KEY < ../../oidc.pem |
| 399 | rm ../../oidc.pem |
| 400 | ``` |
| 401 | |
| 402 | To rotate it, store the current key as `ACTIONS_OIDC_KEY_PREVIOUS`, store |
| 403 | a new one as `ACTIONS_OIDC_KEY`, and delete the previous one a day later. |
| 404 | For a key that may have leaked, skip keeping the previous one. |
| 405 | |
| 406 | ### Telling the status page about deploys |
| 407 | |
| 408 | Restarts during a deploy can look like an outage to the status page's |
| 409 | checks. Set the same value as the status Worker's secret |
| 410 | `STATUS_DEPLOY_TOKEN` and as the repository's Actions secret |
| 411 | `STATUS_DEPLOY_TOKEN`. Deploys then announce their start and finish, and the status page |
| 412 | drafts no incident during them or for 3 minutes after. |
| 413 | |
| 414 | ### Browser push |
| 415 | |
| 416 | `node scripts/ops/vapid-keys.mjs` prints a VAPID key pair. Put the public |
| 417 | half in `VAPID_PUBLIC_KEY` in `services/notify/wrangler.jsonc` and the |
| 418 | private half in the secret `VAPID_PRIVATE_KEY`. |
| 419 | |
| 420 | Pushes to the phone app go through Expo's push service and need nothing |
| 421 | set. If the Expo project the app is built from turns on **enhanced push |
| 422 | security**, put an Expo access token in the notify Worker's secret |
| 423 | `EXPO_ACCESS_TOKEN`: |
| 424 | |
| 425 | ```sh |
| 426 | cd services/notify && npx wrangler secret put EXPO_ACCESS_TOKEN |
| 427 | ``` |
| 428 | |
| 429 | For g1t.sh, leave enhanced push security off: every self-hosted g1t |
| 430 | pushes to the same app, through the same Expo project, without a token of |
| 431 | g1t's. Turning it on stops notifications to phones from every |
| 432 | installation but g1t.sh. |
| 433 | |
| 434 | To send phones nothing at all, set `PHONE_PUSH` to `off` in the `vars` of |
| 435 | both `services/notify/wrangler.jsonc` and `apps/web/wrangler.jsonc`: the |
| 436 | notify Worker sends them nothing, and the site's |
| 437 | `/.well-known/g1t.json` tells the app so. |
| 438 | |
| 439 | ## Rate limits |
| 440 | |
| 441 | Every public surface that costs money or sends something is behind a |
| 442 | [Workers Rate Limiting](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) |
| 443 | binding, with the limits on the [rate limits](/reference/rate-limits/) |
| 444 | page. The binding's `namespace_id` must be unique in your account. A |
| 445 | missing binding, or one that fails, lets requests through. |
| 446 | |
| 447 | ## Spend guardrails |
| 448 | |
| 449 | Cloudflare has no hard spending cap. Besides each workspace's own |
| 450 | [limits](/guides/usage-and-billing/), g1t watches the platform as a whole: |
| 451 | |
| 452 | - **The hourly watch.** At a quarter past each hour, billing reads the |
| 453 | hour before from Cloudflare's GraphQL Analytics API: Workers requests |
| 454 | and CPU, D1 rows, Queue operations, Durable Objects, KV and Artifacts. |
| 455 | It needs `CLOUDFLARE_BILLING_TOKEN` (or `CLOUDFLARE_USAGE_TOKEN`) with |
| 456 | Account Analytics Read. An hour over its threshold, or over ten times |
| 457 | the week's median hour, is emailed to `COSTS_ALERT_EMAIL`. |
| 458 | - **Thresholds** are the `PLATFORM_HOURLY_*` variables in |
| 459 | `services/billing/wrangler.jsonc`; `0` turns one off. A breach at five |
| 460 | times its threshold pauses the kinds of work in `AUTO_PAUSE`. |
| 461 | - **The platform pause** stops a kind of work everywhere: `compute` |
| 462 | (sandboxes), `schedules`, `indexing` or `renders` (social cards). Staff |
| 463 | pause and resume in sudo, under **Costs & margin**. Work already |
| 464 | running finishes, and if the pause cannot be read, nothing is paused. |
| 465 | - **Check the watch's queries** after changing them: |
| 466 | `CLOUDFLARE_API_TOKEN=<token> node scripts/ops/platform-usage.mjs`. It |
| 467 | exits 1 and names any dataset that errored or answered empty. |
| 468 | |
| 469 | In Cloudflare's dashboard, which no code can set, add **Billable Usage** |
| 470 | notifications for each product g1t uses, a monthly **Budget alert**, and a |
| 471 | notification destination that reaches someone. |
| 472 | |
| 473 | ## Logs |
| 474 | |
| 475 | Each Worker keeps a share of its invocations' logs, set by |
| 476 | `head_sampling_rate` in its `wrangler.jsonc`: every one for billing, |
| 477 | identity, runner, actions, deployments, status and sudo, and a tenth for |
| 478 | the rest. Metrics are not sampled. To chase a rare error on a sampled |
| 479 | Worker, raise its rate to 1 while you look. |