Skip to content

Node-first local runtime with optional Cloudflare adapters - #1

Draft
kamath wants to merge 6 commits into
mainfrom
cursor/node-first-cloudflare-optional-7282
Draft

Node-first local runtime with optional Cloudflare adapters#1
kamath wants to merge 6 commits into
mainfrom
cursor/node-first-cloudflare-optional-7282

Conversation

@kamath

@kamath kamath commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Node is the default runtime. Cloudflare is one flag away, on the same code.

pnpm dev                 # frontend on Node
pnpm dev:cf              # frontend on Cloudflare (workerd)
pnpm dev:server          # API on Node
pnpm dev:server:cf       # API on Cloudflare (wrangler dev)

pnpm dev --cloudflare works too — it's a flag on pnpm cli dev, so it shows up in --help instead of being tribal knowledge. Hit /api/stats to see which runtime answered: uptimeMode reports long-running process on Node and per-request isolate on workerd.

Approach

packages/api is the source of truth and stays a plain fetch handler. It reads a small AppBindings object (DATABASE_URL, REGION, RUNTIME) and never imports wrangler, Worker types, or Env. Only the host adapters differ:

Host Node (default) Cloudflare
apps/frontend Vite middleware mounts the API src/server.ts Worker entry
apps/server @hono/node-server (src/node.ts) src/server.ts Worker entry

TARGET=cloudflare selects the Cloudflare pipeline at build/dev time, but the dev:cf, build:cf, and deploy scripts set it, so you shouldn't need to type it. It's deliberately not called RUNTIME — that's the binding reporting which runtime actually served a request.

To add bindings: put them in wrangler.jsonc, run pnpm cf-typegen, and map them onto AppBindings in the adapter (Hyperdrive becomes DATABASE_URL). Never hand-write Env. That same DATABASE_URL slot is the later swap point for PGlite locally and real Postgres in production. Do not auto-migrate on startup.

Two pre-existing bugs fixed along the way

Cloudflare deploy was broken on main. The root wrangler.jsonc points main at src/server.ts, so wrangler rebundled it from source and failed on TanStack Start's virtual modules (#tanstack-start-entry). pnpm deploy now ships the resolved dist/server/wrangler.json the Vite plugin emits.

The CLI crashed at import on Node 22.14. tls.getCACertificates doesn't exist there, and the named import took down every command including pnpm dev. Now a namespace import with feature detection.

Also removed apps/server/tsconfig.worker.json, orphaned by the Node-first switch, and passed --no-install-skills so wrangler never blocks startup on an interactive prompt.

Verification

All four dev paths were started and probed, confirming each reports the expected runtime:

Command uptimeMode
frontend dev long-running process
frontend dev:cf per-request isolate
server dev long-running process
server dev:cf per-request isolate
  • pnpm typecheck and pnpm lint pass, including from a clean checkout with no emitted dist
  • Frontend routes /, /about, /api, /api/stats, /api/openapi.json all 200 on both runtimes
  • Both deploy paths pass wrangler deploy --dry-run (bundled and validated, not deployed)
  • No browser tools were available here, so UI was checked via rendered HTML and HTTP rather than clicking in a browser
Open in Web Open in Cursor 

Keep packages/api as a fetch handler that reads portable AppBindings
instead of Worker types or request.cf. Serve the frontend and standalone
API on Node by default, and leave wrangler.jsonc plus thin Worker
entries as an opt-in deploy path.
Vite's config runner cannot execute the workspace TypeScript package
directly. Dev now ssrLoadModules @template/api, and preview uses tsx,
so typecheck still consumes the API's emitted declarations.
wrangler cannot bundle src/server.ts on its own: the TanStack Start
entry only exists inside the Vite build, so deploying the root
wrangler.jsonc failed on unresolved virtual modules. Ship the resolved
dist/server/wrangler.json the Cloudflare Vite plugin emits instead, and
drop the tsconfig.worker.json left orphaned by the Node-first switch.
tls.getCACertificates and setDefaultCACertificates only exist on newer
Node, so the named import took down every CLI command — including
pnpm dev — on Node 22.14. Import the namespace and feature-detect
instead, skipping the portless CA when the runtime cannot extend it.
Node stays the default, but running on workerd was previously an
undocumented env var and, for the standalone API, not possible at all.
Give both apps a dev:cf script and teach the CLI a --cloudflare flag so
the portless URL and browser handoff are identical either way.

Rename the build-target variable to TARGET so it stops colliding with
the RUNTIME binding that reports which runtime served a request, and
pass --no-install-skills so wrangler never blocks startup on a prompt.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants