> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# cf CLI and Void over Wrangler

> Per-operation parity matrix, native Worker template, rollback, the Wrangler usage guard and the per-service cutover checklist.

## Decision

The target toolchain for active Worker-backed apps and services is **cf + Void**.
Adopt it per verified operation. Preserve existing commands and configuration until
authentication, target identity, bindings and migrations, emitted artifacts and
rollback parity are established. Wrangler remains for recorded gaps with an owner
and removal condition. The shrink-only usage guard requires structured admission
for expansion, changed Wrangler lines, or changed gap scope.

This page refines the cf adoption gates in [Oxc and Cloudflare adoption](/build/development-tools/oxc-cloudflare-adoption.md).
A merged tooling switch is CI-verified, not production-delivered: every deploy
target change still needs the gated production workflow, exact-commit evidence
and the cutover checklist below.

## Toolchain shape

| Surface | Build | Deploy artifact | Publisher |
| - | - | - | - |
| Native Workers (fetch, scheduled) | Vite+ with `@cloudflare/vite-plugin` reading `cloudflare.config.ts` (`experimental.newConfig.cfBuildOutput`) | cf Build Output Specification, `.cloudflare/output/v0` | `cf deploy --prebuilt` |
| Durable Object Workers | Vite+ with the cf config | Same emitted bundle, with retained `v1` migration metadata | Wrangler until a separate rehearsal-backed publication change |
| Framework apps (TanStack Start on Void) | Void SDK plugin on Vite+ | Void-emitted Worker config | Wrangler until the Void publication gap closes |
| OpenNext apps (Parley, Lettin) | OpenNext Cloudflare adapter | OpenNext Worker | Wrangler (adapter-owned) until the app moves to Start on Void |

The Void SDK is not applied to native Workers. Void's supported app types are
Void apps, meta-frameworks and static sites, so a native fetch/scheduled/Durable
Object Worker would have to be rewritten into Void's routes and crons model. For
publication, Void's own Cloudflare deploy converts the emitted config into a cf
Build Output and calls `cf deploy --prebuilt`. Native Workers emit that format
directly, so they share Void's publication path without its runtime layer.

The stable Vite plugin (1.64) already supports `cloudflare.config.ts` and Build
Output emission; the 2.0 beta is not required. Loading `cloudflare.config.ts`
requires Node.js (not Bun). The cf package requires Node `^22.23 || ^24.22 || ^26.12`.

The **cf/Void Wrangler-exit lane** owns the `cf@1.0.0-beta.20` pin.
Upgrade through a scoped dependency PR after checking upstream release notes and
rerunning config/output parity, credential-free artifact checks, secret handling,
settings readback fixtures and real Worker CI tests on the candidate version.
DO publication remains Wrangler until a disposable migration rehearsal and a
separate reviewed publisher change. Renovate holds cf prerelease pins via the
`cf` package rule in `renovate.json`; update this hold only with the qualified
upgrade PR. Do not float this beta pin automatically.

## Operation parity matrix

Verified against cf `1.0.0-beta.20` help, the cf documentation and
`@cloudflare/vite-plugin` 1.64.0 on 2026-10-23.

| Operation | Wrangler | cf / Void | Verdict |
| - | - | - | - |
| Configuration | `wrangler.jsonc` | `cloudflare.config.ts` (`cf/config`), generated by `cf migrate` | Parity, guarded by a normalized identity diff |
| Local development | `wrangler dev` or Vite plugin with `configPath` | `vp dev` with the Vite plugin `newConfig`; `cf dev` also works but delegates to `bunx vite` (stock Vite, not Vite+ core) | Parity |
| Local runtime tests | `createTestHarness` from `wrangler` | `scripts/ci/cf-vite-worker.mjs`: Vite dev server (source) or preview (emitted Build Output) in workerd | Parity for HTTP checks |
| Binding types | `wrangler types` | `cf workers types` writes `.cloudflare/types/index.d.ts` | Parity |
| CI build | Vite plugin emits `dist/*/wrangler.json` | Vite plugin emits `.cloudflare/output/v0` | Parity |
| Credential-free artifact check | `wrangler deploy --dry-run` | `cf deploy --prebuilt --mode production --dry-run --no-provision` | Parity |
| Deploy the exact artifact | `wrangler deploy -c <emitted> --message` | `cf deploy --prebuilt --mode production --no-provision --message` | Parity; `--provision` defaults to true, so always pass `--no-provision` |
| Secrets with a deploy | `wrangler secret bulk` then `wrangler deploy` | `cf deploy --secrets-file` uploads secrets with the same version | Parity (atomic) |
| Bulk secret change | `wrangler secret bulk` | `cf workers secrets bulk --worker <name> --secrets @file` | Parity |
| Single secret put | `wrangler secret put` | Not supported (cf docs) | Gap; use `--secrets-file` or bulk |
| Secret names | `wrangler secret list` | `cf workers secrets list` (API command) | Parity, verify output shape before scripting |
| Versions | `wrangler versions list` | `cf workers versions list --worker-id <name>` | Parity |
| Deployments | `wrangler deployments list` | `cf workers deployments list --worker <name>` | Parity |
| Rollback | `wrangler rollback <version>` | `cf workers deployments create --worker <name> --strategy percentage --versions '[{"version_id":"<id>","percentage":100}]'` | Parity |
| Gradual rollout | `wrangler versions upload` / `versions deploy` | `cf workers versions create --prebuilt`, then `cf workers deployments create` | Parity |
| Cron, routes, custom domains | top-level `triggers`, `routes` | `worker.triggers`, `worker.domains`; `cf workers triggers deploy` | Parity in config; verify applied settings at cutover |
| Durable Object migrations | `migrations` tags | `worker.exports` lifecycle declaration; `cf migrate` does not convert it | Manual conversion; needs a disposable rehearsal before production |
| D1 migrations | `wrangler d1 migrations apply` | `cf d1 migrations apply --dir` (database ID, remote unless `--local`) | Parity, by ID |
| Live logs | `wrangler tail` | Not supported (cf docs) | Gap |
| Prebuilt Void deploy | n/a | `void deploy` always builds and provisions; `--skip-build` is static-only (Void 1.1.12 and 1.2.1) | Gap for the exact-artifact contract |
| OpenNext publication | `opennextjs-cloudflare deploy` invokes Wrangler | None | Gap until Start on Void |

## Native Worker template

Applied first to Cron Control. Each migrated Worker has:

* `cloudflare.config.ts`, generated by `cf migrate --no-install` and reviewed. Durable
  Object `migrations` become `exports: { Class: exports.durableObject({ storage: 'sqlite' }) }`
  for every live class.
* `vite.config.ts` with `cloudflare({ experimental: { newConfig: { cfBuildOutput: true } } })`,
  no `configPath`, explicit preview URL and observability/log settings, loopback dev server settings, `persistState` under a disposable directory for runtime tests (normal dev uses `.cloudflare/state`),
  and the Worker environment's `build.sourcemap: false` (Wrangler did not upload source maps).
* Scripts: `dev: vp dev` (host/port in `vite.config.ts`), `build: vp build`,
  `type-check: cf workers types && tsc`.
  `cf` is an exact devDependency; the app-level `wrangler` devDependency is removed.
* A parity baseline in `scripts/ci/fixtures/cf-worker-parity/<app>.json`, normalized from the
  final `wrangler.jsonc`. `scripts/ci/cf-worker-parity.test.mjs` compares `cloudflare.config.ts`
  with it, and CI compares the emitted Build Output after the build with
  `node scripts/ci/cf-worker-parity.mjs apps/<app> scripts/ci/fixtures/cf-worker-parity/<app>.json`.
* The workflow validates, runs the runtime checks on source and on the emitted Build Output,
  dry-runs `cf deploy --prebuilt`, then uploads `.cloudflare/output` as the exact-commit
  artifact. The gated deploy job downloads that artifact and publishes it without rebuilding.

`@cloudflare/vite-plugin` 1.x still depends on the Wrangler library internally. That is a
library dependency, not CLI usage; it disappears with plugin 2.x.

## Rollback

```bash theme={null}
# Identify the previous version (first deployment is the one serving traffic).
cf workers deployments list --worker tuturuuu-cron-control
# Route 100% of traffic back to it.
cf workers deployments create --worker tuturuuu-cron-control --strategy percentage \
  --versions '[{"version_id":"<previous-version-id>","percentage":100}]'
```

Rollback cannot cross a Durable Object lifecycle change. Secret changes can block
rollback; inspect the error before using `--bypass-deployment-checks`. Retained CI
artifacts (`<app>-worker-<sha>`) allow re-publishing an exact earlier build.

The local harness rejects `.dev.vars*` and `.env*` files in both the app root and
working directory (template `.example` files are allowed). Every declared secret
needs a nonempty disposable fixture. Undeclared/control-key fixtures are rejected,
and declared text vars are masked from the shell before process-env loading. It
restores the first prior environment value after startup
failure or close. `cf-vite-worker.test.mjs` covers both source and artifact modes
using a disposable Vite fixture; real Worker runtime acceptance remains a CI gate.

## Wrangler usage guard

`node scripts/ci/wrangler-usage.js` prints the inventory. The guard scans tracked
files, including executables under `plugins/`, `.agents/` and `tmp/`; Markdown,
text, lockfiles and binary assets are excluded. Ignored/untracked `tmp/` files are
not an inventory: executable files there enter the guard only when tracked.
`.md`, `.mdx` and `.txt` are documentation exclusions in every directory;
commands embedded in those files need normal review, since the guard does not
parse executable documentation examples. It counts mention lines and all
Wrangler token occurrences conservatively, including comments and strings. A SHA-256
digest also covers each file's Wrangler lines (all content for config files), so
same-count substitutions require admission. Lower or remove counts when usage falls.
An `admitted` record must be `{ "decision": "https://github.com/tutur3u/platform/pull/<id>",
"reason": "<specific reviewed change>" }`. New entries, growth, changed digests and
changed gap scope need a fresh record; reverting a migration follows the same rule.
CI
compares the allowlist with its base commit (`HEAD^1` of the pull request merge ref)
and fails if that commit is unavailable.

Current gaps: `paused-runtime` (retained paused runtimes), `opennext-adapter`,
`void-wave1-publication`, `pending-migration` (services awaiting their parity PR)
`shared-toolchain` (root CLI provider, trusted-dependency and policy files) and
`parity-tooling` (the migration parity checker, which reads former Wrangler configs).

## Service inventory

| Service | Current publisher | Status | Gap |
| - | - | - | - |
| Cron Control | cf (after its parity PR) | Template | — |
| Devbox Control, Coordination | Wrangler | Template extension with DO `exports` | `pending-migration` |
| Colab | Wrangler bundler with assets and AI | Needs Vite build-output template | `pending-migration` |
| Meet realtime | Wrangler, three SQLite DO classes | Needs DO `exports` and runtime fixtures | `pending-migration` |
| Mail email routing | Wrangler config, email handler | Needs the email trigger mapping | `pending-migration` |
| Meet, Apps, Calendar, Mail, Shortener | Void build, Wrangler publication | Wave 1 owner | `void-wave1-publication` |
| Parley, Lettin | OpenNext, Wrangler | Start on Void migration | `opennext-adapter` |
| Backend, TanStack Web | Paused | Excluded | `paused-runtime` |

## Settings preflight token permissions

The deploy token must cover the pinned account and every zone in that account.
The settings snapshots require these Cloudflare API token permissions in addition
to the publication permissions:

| Resource | Read permission | Snapshot data |
| - | - | - |
| Target account | Account → Workers Scripts → Read (Write also grants read) | Worker settings, schedules, secret names, deployments and custom domains |
| Every target-account zone | Zone → Zone → Read | Zone enumeration |
| Every target-account zone | Zone → Workers Routes → Read | Worker route enumeration |

Grant zone visibility for **all zones in the target account**. Limiting visibility
to the expected hostname's zone can hide unexpected routes and cannot establish
route parity. Do not grant unrelated accounts. These requirements apply to
`COLAB_CLOUDFLARE_API_TOKEN` and to `COORDINATION_CLOUDFLARE_API_TOKEN` when supplied
instead of the fallback. Keep values in GitHub secrets; never attach token values
to receipts. Actual token scope remains unverified until the bounded read-only
preflight succeeds. Missing access, truncated lists and unsupported account size
fail before publication; do not bypass that gate to deploy.

Cloudflare documents the accepted permissions for
[Worker secrets](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/secrets/methods/list/),
[deployments](https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/deployments/methods/list/),
[custom domains](https://developers.cloudflare.com/api/resources/workers/subresources/domains/methods/list/),
[zones](https://developers.cloudflare.com/api/resources/zones/methods/list/) and
[routes](https://developers.cloudflare.com/api/resources/workers/subresources/routes/methods/list/).

## Deployment account variable

Set the non-secret **Actions repository variable** `CLOUDFLARE_ACCOUNT_ID` in
`tutur3u/platform` (Settings → Secrets and variables → Actions → Variables).
Use the account ID recorded in the service's `cloudflare.config.ts` and parity
baseline; the deploy job checks it against the emitted artifact. One repository
variable supplies `cron-control-production`, `devbox-control-production` and
`coordination-production`. If any of those environments defines the same variable,
it must have the same value because the environment value takes precedence.
Missing or mismatched configuration blocks deployment. Source publication does
not provision this variable; the release owner must set it before promotion.

## Cached Build Output

The root Turbo `build.outputs` includes `.cloudflare/output/**`, including hidden
Vite metadata. Cache hits must restore the complete emitted tree before parity,
runtime and publication checks. `.cloudflare/state` remains outside the output
scope. `cf-build-output-cache.test.mjs` runs a disposable emit-only task through
an actual cache miss and hit, verifies restored bytes and execution count, and
checks the old output list as a negative control. It never compiles an app or
uses shared/remote cache credentials.

## Production cutover checklist

For each service, the orchestrator verifies before promotion:

1. Exact-commit CI on the promoted SHA is green for the service workflow, including:
   source runtime checks, emitted Build Output parity
   (`cf-worker-parity.mjs` prints `matches <worker> deployment identity`), emitted runtime
   checks, the `cf deploy --prebuilt --dry-run --no-provision` step, and the uploaded
   `<app>-worker-<sha>` artifact.
2. The production job publishes that artifact with `cf deploy --prebuilt` (no rebuild) and
   the same `--message` SHA.
3. The deploy job runs `cf-worker-settings.mjs before` before any publication or
   secret mutation, then `after` before recording success. It checks the pinned
   account, compatibility, preview URLs, observability/logs, cron schedules and
   declared secret names; snapshots routes/custom domains and hashes remaining
   settings without persisting text bindings. It captures the active deployment and version IDs
   (`cf workers deployments list`) as the rollback target. Also capture `cf workers get <name>`,
   `cf workers secrets list` and, with the Cloudflare API, the script's cron schedules
   (`/workers/scripts/<name>/schedules`), routes or custom domains, and `workers.dev`/preview
   URL state. The job captures them again after deployment and compares; unexplained changes
   fail the job before its success marker. Evidence is retained even on failure.
   This route-free pilot supports at most 20 account zones and one API list page,
   uses at most 28 GET requests per snapshot, and fails closed on missing read
   permissions, truncated results or unsupported scope. It never retries writes.
   The expected preview setting is explicitly true; this is unverified against
   current production until the preflight runs. A mismatch stops before mutation.
4. Production publication for Devbox Control and Coordination remains Wrangler
   with their applied `v1` migration metadata. A separate reviewed change must
   carry the rehearsal receipt before enabling cf publication. For these Workers, rehearse the first cf deploy on a disposable Worker created
   with the same Wrangler migration history and data, and confirm object data survives the
   switch to `exports`. Do not promote until that rehearsal passes.
5. Health checks pass on the canonical host, and the deployment marker records the SHA.
6. If a check fails, roll back with `cf workers deployments create` to the captured
   version, then reconcile before retrying.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.