Skip to content
Launch
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.

  1. Each Worker app has a cloudflare.config.ts (the cf CLI’s config). No wrangler.jsonc, wrangler.toml or Wrangler dependency. Worker name, compatibility date and bindings live there; the account comes from CLOUDFLARE_ACCOUNT_ID, never from the file.
  2. Secrets are declared by name with bindings.secret(). Values come from the shell or CI secrets at deploy time and from a gitignored .env.local locally.
  3. Deploy is vite build followed by cf deploy --prebuilt. scripts/deploy.mjs in the spike app does this. If the secret values are in the environment it uploads them with the version through a temporary 0600 --secrets-file; if none are, the Worker keeps the secrets it has, which pnpm run secrets sets (cf workers secrets bulk). --preview builds with CLOUDFLARE_PREVIEW_BUILD=true and runs cf previews deploy --prebuilt, which names the Preview after the Git branch. These scripts should move to a shared package once a second Worker exists.
  4. CI is Workers Builds, not GitHub Actions: see 11-workers-builds.md. Local deploys use cf auth login or CLOUDFLARE_API_TOKEN (the “Edit Cloudflare Workers” token template covers it).
  5. Typecheck with cf workers types && tsc --noEmit. Generated types land in .cloudflare/types and 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() or headers() as “Unknown”; that is a build report label only, and the routes cached correctly at runtime.
  • The upstream fetch to 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 exactOptionalPropertyTypes from the base config, because vinext 1.4’s adapter descriptor types do not satisfy it.
  • In this sandbox, vite preview needed NO_PROXY=localhost,127.0.0.1,::1: miniflare’s internal fetch to workerd fails with the sandbox’s long NO_PROXY list. Normal machines are not affected.

Was this page helpful?