I was building an application using Nuxt, with Cloudflare Workers handling the backend and D1 providing the database.
Everything worked reasonably well when deployed to Cloudflare, but local development was a different story.
Normally, I can run pnpm dev and Nuxt will start a development server, complete with hot module replacement.
The problem was that my application relied on Cloudflare-specific bindings, particularly the D1 database.
Those bindings exist in Cloudflare’s Workers runtime, but they aren’t automatically available when running an ordinary Node.js development server.
I wanted to be able to use Nuxt’s normal development workflow without having to deploy the application every time I made a change.
The problem with Cloudflare bindings
Cloudflare Workers can access resources such as D1, KV and R2 through bindings configured in Wrangler.
For example, my application has a D1 binding called DB.
1[[d1_databases]]2binding = "DB"3database_name = "tv_shows"4database_id = "<DATABASE_ID>"
When the application runs on Cloudflare, that binding is available to the Worker.
I can then create a Drizzle database instance using the binding.
1import { drizzle } from 'drizzle-orm/d1'2 3export function useDb() {4 return drizzle(__env__.DB)5}
The problem is that __env__.DB isn’t a normal environment variable.
It’s a Cloudflare runtime binding containing a database interface.
Setting DB=some-value in a .env file won’t reproduce it.
I needed my local development environment to provide the same interface as Cloudflare.
Using Nitro’s Cloudflare preset
Nuxt uses Nitro to build its server-side functionality.
Nitro supports different deployment targets, including Cloudflare Workers.
My application uses the cloudflare_module preset.
1export default defineNuxtConfig({2 nitro: {3 preset: 'cloudflare_module'4 }5})
This allows Nuxt to build the server into a format that can run in Cloudflare’s Workers environment.
It also means I can write normal Nuxt server routes while deploying them to Cloudflare.
For example, an endpoint under server/api/shows.get.ts becomes an API route without having to create a separate backend application.
However, simply configuring the Cloudflare preset doesn’t make all of Cloudflare’s bindings available when running nuxt dev.
Adding nitro-cloudflare-dev
I found the nitro-cloudflare-dev package, which was designed to bridge this gap.
It uses Cloudflare’s local development tooling to provide bindings to Nitro during development.
I added it as a Nuxt module:
1export default defineNuxtConfig({2 modules: [3 'nitro-cloudflare-dev'4 ]5})
With the D1 database configured in wrangler.toml, the development environment can expose the database binding to the application.
That means I can continue using the same database helper locally and in production, rather than maintaining two implementations.
I don’t need to rewrite all my queries to use SQLite directly in development and D1 when deployed.
The application uses Drizzle in both environments.
Managing local database migrations
Having a local D1 database is useful, but it also needs the same schema as the deployed database.
I use Drizzle Kit to generate the migrations, then Wrangler to apply them.
For local development:
1wrangler d1 migrations apply tv_shows --local
For the deployed database:
1wrangler d1 migrations apply tv_shows --remote
These are deliberately separate commands.
I don’t want a development session accidentally changing the production database just because I’m testing a new feature.
I also added scripts to package.json to make the commands easier to remember.
1{2 "scripts": {3 "migrations:generate:schema": "drizzle-kit generate --schema=server/db/schema.ts --dialect=sqlite",4 "migrations:list:local": "wrangler d1 migrations list tv_shows --local",5 "migrations:apply:local": "wrangler d1 migrations apply tv_shows --local",6 "migrations:list:remote": "wrangler d1 migrations list tv_shows --remote",7 "migrations:apply:remote": "wrangler d1 migrations apply tv_shows --remote"8 }9}
Keeping these commands in the project means I don’t have to look up the Wrangler syntax each time I change the database schema.
Handling secrets locally
The application also needs environment variables for authentication and external services.
Cloudflare supports a .dev.vars file for local development secrets.
For example:
1NUXT_AUTH_JS_SECRET=replace-with-a-random-secret2NUXT_PUBLIC_AUTH_JS_BASE_URL=http://localhost:30003DB_LOGGING=false
The Auth.js secret is particularly important because it’s used when handling authentication.
I didn’t want a developer to clone the repository, install the dependencies and immediately run into errors because the secret was missing.
I added a post-installation script which creates .dev.vars if it doesn’t already exist.
1import { existsSync, writeFileSync } from 'node:fs' 2import { randomBytes } from 'node:crypto' 3import { resolve } from 'node:path' 4 5const file = resolve(process.cwd(), '.dev.vars') 6 7if (!existsSync(file)) { 8 const secret = randomBytes(32).toString('base64') 9 10 writeFileSync(11 file,12 `NUXT_AUTH_JS_SECRET=${secret}\n`,13 { flag: 'wx' }14 )15}
The actual implementation tries OpenSSL first and falls back to Node’s crypto module if OpenSSL isn’t available.
It also writes the local authentication URL and database logging setting.
Importantly, the script leaves the file alone if it already exists, so running pnpm install again won’t overwrite the developer’s configuration.
The file is excluded from Git, so the generated secret isn’t committed to the repository.
Making the Cloudflare Cache API work
There was another issue when I started using Cloudflare’s Cache API.
Cloudflare Workers expose a global caches object, but that isn’t available in a normal Node.js environment.
The nitro-cloudflare-dev package was already creating a proxy to the Cloudflare environment, but it only exposed the environment bindings.
I needed the Cache API as well.
I ended up patching the package.
The original code exposed the environment through the development proxy:
1globalThis.__env__ = proxy.env
I extended it to expose the cache interface too:
1globalThis.__env__ = proxy.env2globalThis.caches = proxy.caches
The patch also handled the initial asynchronous proxy setup:
1globalThis.__env__ = _proxy.then(2 proxy => proxy.env3)4 5globalThis.caches = _proxy.then(6 proxy => proxy.caches7)
This allowed the development environment to access the cache interface provided by the Cloudflare runtime proxy.
It fixed a problem where the application could use a Cloudflare feature in production while the local runtime wasn’t exposing the same functionality.
Keeping the patch in the repository
One thing I didn’t want to do was manually edit files inside node_modules.
Any changes would disappear the next time I installed dependencies.
I use pnpm, which supports patching dependencies.
The patch is stored in the repository under patches/, and package.json tells pnpm to apply it.
1{2 "pnpm": {3 "patchedDependencies": {4 "nitro-cloudflare-dev@0.2.2": "patches/nitro-cloudflare-dev@0.2.2.patch"5 }6 }7}
This means the workaround is reproducible.
When the dependencies are installed, pnpm applies the patch automatically.
It’s still something I’d prefer to remove eventually, particularly if the upstream package adds the required support, but it’s much better than having an undocumented modification to an installed dependency.
Keeping development and production consistent
The main benefit of this setup is that I can develop locally using Nuxt’s usual tooling while accessing the same types of Cloudflare resources that the deployed application uses.
I can run the application locally, query D1, test authentication and work on API routes without deploying every change.
It isn’t a perfect reproduction of production.
The local database is separate, and there can still be differences in how the development runtime handles Workers-specific features.
But it’s close enough to make development practical.
The important thing is that the application code doesn’t need to know whether it’s running locally or on Cloudflare.
The database helper still uses the D1 binding. The authentication system still uses the Drizzle adapter. The API routes still run through Nitro and are cached.
Most of the differences are handled by the development tooling.
What I’d do differently?
The main thing I’d like to avoid is maintaining patches against development dependencies.
They’re useful when you need a workaround, but they can become awkward when upgrading packages.
I’d also make sure the local environment is tested regularly against the deployed runtime, particularly when changing Nuxt, Nitro or Wrangler versions.
A feature working in nuxt dev doesn’t guarantee it will behave exactly the same when deployed to Cloudflare.
For this project, though, the setup solved the problem I had.
I could keep using Nuxt’s development server while building an application that relied on Cloudflare’s database and runtime APIs.
It’s a relatively small part of the project, but getting the local development environment right made everything else much easier to work on.
The source code, including the development tooling and dependency patches, is available on GitHub