vinext on Workers without Wrangler config
Issue: #2. The spike app, apps/spike-vinext, was removed once apps/web-wordpress took over its Worker and setup (see the git history for it).
Answer
Yes. vinext 1.4 deploys to Workers through the new cf CLI with no Wrangler at all: Wrangler is not even in the dependency tree. Worker settings live in cloudflare.config.ts, the cf CLI’s typed config file, and cf deploy --prebuilt uploads the Cloudflare Build Output that vite build writes to .cloudflare/output/. This is now vinext’s default setup (create-vinext-app and vinext init --platform=cloudflare); the Wrangler path is the opt-in --legacy-wrangler-cloudflare-init.
Every feature the front-ends need worked in local workerd (vite preview), with one caveat on remote images and one on edge caching, below. The pattern is now live: apps/web-wordpress deploys this way as the launch-vinext-wordpress Worker at https://launch-wordpress.patronage.dev, and apps/payload and apps/docs-site use the same cf deploy --prebuilt path (2026-10-30).
Versions tested
vinext 1.4.0, @vinext/cloudflare 1.3.0, cf 1.0.0-beta.25, @cloudflare/vite-plugin 2.0.0-beta.v.7 (v.8 was inside the 3-hour release-age window), Vite 8.3.0, React 19.3.1, TypeScript 7.0.2. cf and the Vite plugin v2 are both betas.
Results
| Feature | Result | Notes |
|---|---|---|
| App Router page | Works | |
| Dynamic route | Works | ISR needs generateStaticParams (it may return []), the same rule as Next.js. Without it the route renders on every request with no-store. |
| API routes | Works | |
ISR (revalidate) |
Works | Served from KV: x-vinext-cache: MISS, then HIT with s-maxage. |
revalidateTag |
Works | A tag set with cacheTag() inside "use cache", or with fetch(url, { next: { tags } }), also invalidates every ISR page built from that data. One call refreshes both. |
revalidatePath |
Works | |
| Draft mode | Works | draftMode().enable() sets __prerender_bypass. Draft requests skip the cache (no-store) and never write to it; anonymous requests still get the cached page. |
fetch caching |
Works | Tagged fetches are shared across pages and cleared by revalidateTag. |
next/image, local |
Works | /_next/image resizes through the Images binding (AVIF/WebP, immutable cache headers), simulated locally. |
next/image, remote |
Partial | Remote URLs go through @unpic/react, which rewrites them to the source CDN’s own resize parameters when it recognises the host, and does not use the Images binding. Spike 3 (headless WordPress) should check what this does with WordPress media URLs; the likely fix is a custom loader that uses Cloudflare image transformations. |
| Plain-text vars | Works | bindings.text(value) in cloudflare.config.ts. Read with import { env } from "cloudflare:workers"; also mirrored onto process.env. |
| Secrets | Works | bindings.secret() declares the name; values come from .env.local locally and from cf deploy --secrets-file at deploy time. |
| KV namespace | Works (dry run) | cf deploy --provision (on by default) creates it on first deploy, so there is no manual setup step. |
Caching mode
create-vinext-app offers several page caches. The spike started with workersCacheCdnAdapter() (edge ISR in Workers Cache, purged by tag), but its pages came back uncached in local preview: Workers Cache is not simulated locally, and cacheability is decided by a deploy-time probe. That cannot be tested without an account.
The spike now uses the KV data adapter for both data and pages (--cdn-cache data-cache --data-cache kv). Everything is testable locally, and one revalidateTag clears data and pages together, which is the behaviour the CMS webhooks need. Once a deploy is possible, it is worth timing KV against Workers Cache on a real zone; switching is a change to vite.config.ts and cloudflare.config.ts only.
Recommended deploy pattern for every Worker in this repo
- Each Worker app has a
cloudflare.config.ts(thecfCLI’s config). Nowrangler.jsonc,wrangler.tomlor Wrangler dependency. Worker name, compatibility date and bindings live there; the account comes fromCLOUDFLARE_ACCOUNT_ID, never from the file. - Secrets are declared by name with
bindings.secret(). Values come from the shell or CI secrets at deploy time and from a gitignored.env.locallocally. - Deploy is
vite buildfollowed bycf deploy --prebuilt.scripts/deploy.mjsin the spike app does this. If the secret values are in the environment it uploads them with the version through a temporary0600--secrets-file; if none are, the Worker keeps the secrets it has, whichpnpm run secretssets (cf workers secrets bulk).--previewbuilds withCLOUDFLARE_PREVIEW_BUILD=trueand runscf previews deploy --prebuilt, which names the Preview after the Git branch. These scripts should move to a shared package once a second Worker exists. - CI is Workers Builds, not GitHub Actions: see
11-workers-builds.md. Local deploys usecf auth loginorCLOUDFLARE_API_TOKEN(the “Edit Cloudflare Workers” token template covers it). - Typecheck with
cf workers types && tsc --noEmit. Generated types land in.cloudflare/typesand stay gitignored.
The vinext-cloudflare deploy wrapper also uses cf, but it cannot pass --secrets-file, so the repo script calls cf deploy directly. The wrapper is only needed for its cache warming flags, which the KV mode does not use.
Other resources outside a Worker’s bindings (DNS, Access applications, custom domains) are a separate question for the Access spike; cf covers the whole Cloudflare API (cf cli search "<task>" finds the command), so those can be scripted the same way.
Gaps and follow-ups
- Run one live deploy with an account token, and check ISR headers, KV provisioning and the Images binding on
workers.dev. - vinext build output marks routes that call
draftMode()orheaders()as “Unknown”; that is a build report label only, and the routes cached correctly at runtime. - The upstream
fetchto the public internet failed in this sandbox (no direct egress from workerd), so the fetch cache was tested against the Worker’s own/api/now. - The app’s tsconfig turns off
exactOptionalPropertyTypesfrom the base config, because vinext 1.4’s adapter descriptor types do not satisfy it. - In this sandbox,
vite previewneededNO_PROXY=localhost,127.0.0.1,::1: miniflare’s internal fetch to workerd fails with the sandbox’s longNO_PROXYlist. Normal machines are not affected.