Describe the bug
With nitro.preset: 'cloudflare-module' and hub: { db: 'sqlite' }, the generated Drizzle client resolves to a different driver depending on nuxt.options.dev — libsql → .data/db/sqlite.db in dev, d1 → the miniflare binding otherwise. That choice is baked into a single shared file, node_modules/@nuxthub/db/db.mjs, which nitro dev imports by absolute path (.nuxt/dev/index.mjs contains a literal import { db } from "file:///…/node_modules/@nuxthub/db/db.mjs").
So any second Nuxt instance loaded with dev: false overwrites that file while nuxt dev is running. In my project the writers are vitest (defineVitestProject → loadNuxt({ dev: false }) → buildNuxt), nuxt prepare from a postinstall, nuxt db generate, and nuxt build.
The dev server keeps the old module in Node's ESM registry until its next module-graph reload — in practice Vite's new dependencies optimized: … — then re-reads the file from disk and starts querying a completely different, empty database. migrations.dev then applies every migration there, so the schema is complete and all queries succeed with zero rows. There is no error and no stack trace; the app just 404s on every authenticated request until the dev server is restarted.
I ended up with two fully-migrated databases (17 rows in _hub_migrations each) and no indication which one was live:
| File |
Driver |
users rows |
.data/db/sqlite.db |
libsql |
2 — the real dev data |
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/cfb6….sqlite |
d1 |
0 — empty |
Caught in the act via mtimes:
21:56:02 .data/db/sqlite.db ← dev working, libsql, users = 2
22:05:08 node_modules/@nuxthub/db/db.mjs ← rewritten to the d1 variant
22:05:08 .data/content/contents.sqlite ← full buildNuxt (the vitest run)
22:08:26 .nuxt/dev/index.mjs ← dev reload re-imports db.mjs
22:08:59 .wrangler/state/v3/d1/…/*.sqlite ← migrations.dev creates 47 tables here
The running server's baked runtimeConfig.hub.db says driver:"libsql" while the db.mjs it imports is the d1 variant — the two disagree in the same tree, and nothing detects it.
Root cause, all references node_modules/@nuxthub/core/dist/module.mjs at 0.10.8:
-
Mode-dependent artifact in a shared location. setupDatabaseClient writes to a fixed path (:693):
const physicalDbDir = join(nuxt.options.rootDir, "node_modules", "@nuxthub", "db");
await writeFile(join(physicalDbDir, "db.mjs"), drizzleOrmContent.replace(...));
Concurrent Nuxt instances race over it; last writer wins. Nothing keys the path on build mode, buildId, or buildDir.
-
Nitro dev externalizes it, so disk state wins on reload. There is no invalidation, and no check that the imported client still matches the runtimeConfig.hub.db the bundle was built with.
-
driver: 'libsql' cannot be pinned to opt out. In resolveDatabaseConfig the cloudflare/!dev branch is evaluated before the explicit-libsql branch (:227-243):
const userExplicitLibsql = config.driver === "libsql";
...
if (config.driver === "d1") { break; }
if (hub.hosting.includes("cloudflare") && !nuxt.options.dev) {
config.driver = "d1"; // ← wins
break;
}
if (userExplicitLibsql) { // ← unreachable when !dev on cloudflare hosting
config.connection = defu(config.connection, { url: "" });
break;
}
driver: 'd1' is honoured in every mode; driver: 'libsql' is not.
-
hosting is preset-derived, so this fires on a laptop (:1073):
const hosting = process.env.NITRO_PRESET || nuxt.options.nitro.preset || provider;
Setting nitro.preset: 'cloudflare-module' in nuxt.config.ts, rather than relying on deploy-time env detection, makes hosting cloudflare locally too — which is what activates the dev/non-dev divergence on a dev machine.
Environment
|
|
@nuxthub/core |
0.10.8 |
nuxt |
4.5.2 |
nitropack |
2.13.4 |
@nuxt/test-utils |
4.1.0 |
drizzle-orm |
0.45.2 |
@libsql/client |
0.17.4 |
wrangler |
4.125.0 |
vite |
8.2.1 |
| Node / npm |
v26.7.0 / 11.19.0 |
| Config |
hub: { db: 'sqlite', kv: true, cache: true, blob: true }, nitro.preset: 'cloudflare-module' |
Steps to reproduce
- Create a Nuxt 4 project with
nitro.preset: 'cloudflare-module' and hub: { db: 'sqlite' }, plus a vitest.config.ts using defineVitestProject from @nuxt/test-utils/config.
- Run
nuxt dev and insert a row (e.g. a user). Confirm grep -c "drizzle-orm/libsql" node_modules/@nuxthub/db/db.mjs returns 1 — dev wrote the libsql client — and that reads work.
- Leave the dev server running. In a second terminal run
vitest run. It calls loadNuxt({ dev: options.overrides?.dev ?? false }) (@nuxt/test-utils/dist/config.mjs:29) then buildNuxt, so setupDatabaseClient runs with dev: false. grep -c "drizzle-orm/d1" node_modules/@nuxthub/db/db.mjs now returns 1 — the file has been rewritten under the running dev server.
- Back in the dev server, trigger anything that makes Vite re-optimize deps and reload the module graph (importing a not-yet-optimized dependency, or touching a file that pulls one in). Watch for
new dependencies optimized: ….
- Re-request any route that reads the database. Every query now returns zero rows — the row inserted in step 2 is gone, with no error logged.
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite has just been given the full schema by migrations.dev.
- Restart
nuxt dev. The libsql client is regenerated and the data reappears, which makes the whole thing look like a caching or auth bug rather than a database one.
Expected behavior
A running nuxt dev server should keep talking to the database it started with. A concurrent non-dev Nuxt load — a test run, a postinstall nuxt prepare, nuxt db generate, nuxt build — should not be able to repoint it at a different database, and certainly not silently.
Describe the bug
With
nitro.preset: 'cloudflare-module'andhub: { db: 'sqlite' }, the generated Drizzle client resolves to a different driver depending onnuxt.options.dev—libsql→.data/db/sqlite.dbin dev,d1→ the miniflare binding otherwise. That choice is baked into a single shared file,node_modules/@nuxthub/db/db.mjs, which nitro dev imports by absolute path (.nuxt/dev/index.mjscontains a literalimport { db } from "file:///…/node_modules/@nuxthub/db/db.mjs").So any second Nuxt instance loaded with
dev: falseoverwrites that file whilenuxt devis running. In my project the writers arevitest(defineVitestProject→loadNuxt({ dev: false })→buildNuxt),nuxt preparefrom apostinstall,nuxt db generate, andnuxt build.The dev server keeps the old module in Node's ESM registry until its next module-graph reload — in practice Vite's
new dependencies optimized: …— then re-reads the file from disk and starts querying a completely different, empty database.migrations.devthen applies every migration there, so the schema is complete and all queries succeed with zero rows. There is no error and no stack trace; the app just 404s on every authenticated request until the dev server is restarted.I ended up with two fully-migrated databases (17 rows in
_hub_migrationseach) and no indication which one was live:usersrows.data/db/sqlite.dblibsql.wrangler/state/v3/d1/miniflare-D1DatabaseObject/cfb6….sqlited1Caught in the act via mtimes:
The running server's baked
runtimeConfig.hub.dbsaysdriver:"libsql"while thedb.mjsit imports is thed1variant — the two disagree in the same tree, and nothing detects it.Root cause, all references
node_modules/@nuxthub/core/dist/module.mjsat0.10.8:Mode-dependent artifact in a shared location.
setupDatabaseClientwrites to a fixed path (:693):Concurrent Nuxt instances race over it; last writer wins. Nothing keys the path on build mode,
buildId, orbuildDir.Nitro dev externalizes it, so disk state wins on reload. There is no invalidation, and no check that the imported client still matches the
runtimeConfig.hub.dbthe bundle was built with.driver: 'libsql'cannot be pinned to opt out. InresolveDatabaseConfigthe cloudflare/!devbranch is evaluated before the explicit-libsql branch (:227-243):driver: 'd1'is honoured in every mode;driver: 'libsql'is not.hostingis preset-derived, so this fires on a laptop (:1073):Setting
nitro.preset: 'cloudflare-module'innuxt.config.ts, rather than relying on deploy-time env detection, makeshostingcloudflare locally too — which is what activates the dev/non-dev divergence on a dev machine.Environment
@nuxthub/corenuxtnitropack@nuxt/test-utilsdrizzle-orm@libsql/clientwranglervitehub: { db: 'sqlite', kv: true, cache: true, blob: true },nitro.preset: 'cloudflare-module'Steps to reproduce
nitro.preset: 'cloudflare-module'andhub: { db: 'sqlite' }, plus avitest.config.tsusingdefineVitestProjectfrom@nuxt/test-utils/config.nuxt devand insert a row (e.g. a user). Confirmgrep -c "drizzle-orm/libsql" node_modules/@nuxthub/db/db.mjsreturns1— dev wrote the libsql client — and that reads work.vitest run. It callsloadNuxt({ dev: options.overrides?.dev ?? false })(@nuxt/test-utils/dist/config.mjs:29) thenbuildNuxt, sosetupDatabaseClientruns withdev: false.grep -c "drizzle-orm/d1" node_modules/@nuxthub/db/db.mjsnow returns1— the file has been rewritten under the running dev server.new dependencies optimized: …..wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlitehas just been given the full schema bymigrations.dev.nuxt dev. The libsql client is regenerated and the data reappears, which makes the whole thing look like a caching or auth bug rather than a database one.Expected behavior
A running
nuxt devserver should keep talking to the database it started with. A concurrent non-dev Nuxt load — a test run, apostinstallnuxt prepare,nuxt db generate,nuxt build— should not be able to repoint it at a different database, and certainly not silently.