跪拜 Guibai
← All articles
Frontend

Electron + Drizzle + SQLite: Skip the Native Addon Hell, Use node:sqlite

By 律宏阔 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Electron apps that need a local database can now skip the entire `better-sqlite3` → `electron-rebuild` → ASAR unpack pipeline. The built-in `node:sqlite` driver works today, but the Vite bundler and Forge packager will silently break module resolution and file paths unless three specific config fixes are applied.

Summary

The combination of Electron Forge, Vite, and Drizzle ORM hits two distinct failure modes when connecting to SQLite. First, Vite treats Node's built-in `node:sqlite` module as a browser external, so `DatabaseSync` disappears from the bundle unless the module is explicitly listed in Rollup's `external` config. Second, and more insidious, the migration folder path resolves to `/drizzle` — the OS root — because `__dirname` inside a Vite-bundled main process no longer points to the project source tree.

Switching to `better-sqlite3` only trades one problem for a longer chain: dynamic `.node` require failures in the Vite bundle, missing modules after `electron-forge make`, and the realization that `@electron-forge/plugin-auto-unpack-natives` handles ASAR unpacking but does not copy the entire npm package into the final app. The built-in `node:sqlite` driver sidesteps all of this — no native addon, no `electron-rebuild`, no ABI mismatches.

The remaining fixes are straightforward: mark `node:sqlite` as external in the Vite config, use `app.getAppPath()` in development and `process.resourcesPath` in production to locate migrations, ship the `drizzle/` folder via Forge's `extraResource`, and store the SQLite database file in `app.getPath('userData')` rather than a relative path or the Documents folder.

Takeaways
Vite bundles `node:sqlite` as a browser external by default; add `'node:sqlite'` to `rollupOptions.external` in `vite.main.config.ts` so Electron's Node runtime loads it.
`better-sqlite3` in an Electron+Vite project triggers a cascade: dynamic `.node` require failures, missing module after `electron-forge make`, and the `auto-unpack-natives` plugin only unpacks native files already inside the ASAR — it does not copy the whole package into the app.
`__dirname` inside a Vite-bundled main process points to the bundle output directory (e.g., `.vite/build`), not the project source; using `../../drizzle` to reach the project root can resolve to `/drizzle` and cause `ENOENT`.
For migration paths, use `app.getAppPath()` in development and `process.resourcesPath` in production, combined with Forge's `extraResource: ['./drizzle']` to ship migration files inside `Contents/Resources/drizzle/`.
Store the SQLite database file in `app.getPath('userData')`, not in a relative path, `__dirname`, or `app.getPath('documents')`, to survive packaging and app upgrades.
Initialize the database once at app startup rather than scattering it across IPC handlers; call `migrate(db, { migrationsFolder })` on first launch to create tables and on subsequent launches to apply new migrations.
Conclusions

The `node:sqlite` built-in module has reached the point where it eliminates the most painful part of Electron database setup — the native addon rebuild chain — yet most Electron + SQLite guides still default to `better-sqlite3` out of habit.

Vite's `external` config solves the bundling problem but creates a packaging problem Forge doesn't automatically handle; the `auto-unpack-natives` plugin name is misleading because it unpacks files already present, rather than ensuring external dependencies are present in the first place.

The migration path bug (`/drizzle`) is a concrete example of how Vite's bundling silently relocates `__dirname`, making relative-path assumptions from Node.js tutorials actively dangerous in bundled Electron main processes.

Separating migration files (shipped as app resources) from the database file (stored in user data) is the correct architecture, but Forge's `extraResource` and Electron's `process.resourcesPath` are the only reliable way to make that separation survive packaging.

Concepts & terms
node:sqlite
A built-in Node.js module (available in Node 22+ and Electron versions that ship it) providing a synchronous SQLite interface via `DatabaseSync`. Requires no native addon compilation, unlike `better-sqlite3`.
Vite external
A Rollup `external` option that tells Vite not to bundle a module into the output file, leaving it to be resolved at runtime by the host environment (Node or Electron). Required for native modules and Node built-ins like `node:sqlite`.
ASAR unpack
Electron's ASAR archive format cannot memory-map native `.node` files directly; they must be extracted to an `app.asar.unpacked` directory. The `auto-unpack-natives` plugin automates this extraction but does not ensure the native package is included in the app at all.
extraResource (Electron Forge)
A Forge packager config that copies specified files or directories from the project into the packaged app's `Resources/` folder, accessible at runtime via `process.resourcesPath`. Used here to ship Drizzle migration files alongside the app binary.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗