Skip to content

Replace better-sqlite3 with the built-in node:sqlite #1237

Description

@sonukapoor

Note: this is an in-house item already being handled by the maintainer - not open for contribution. Filed for tracking only.

better-sqlite3 is our only native-compiled dependency. Node now ships SQLite as a core module, so we can stop shipping a compiled copy of one.

Why

The native build is the cause of every install complaint we get. It is what produces the blocked-install-script warnings under npm 12, the prebuild-install@7.1.3 deprecation notice, and the recurring EBADENGINE friction whenever a new Node major lands before better-sqlite3 declares support for it.

Worth being precise about where the noise comes from: cve-lite-cli itself declares no install-time hooks at all, no preinstall, install, postinstall or prepare. The only install script in the tree is better-sqlite3's prebuild-install || node-gyp rebuild --release. Reported as a symptom in #1223.

Why this is cheap

Measured rather than assumed:

  • better-sqlite3 is imported in exactly one file, src/advisory/local-db.ts.
  • node:sqlite opens our existing 498MB database unmodified, with the same SQL. No migration, no re-sync.
  • Both query shapes work: the (ecosystem, package_name) lookup returns in about 1ms, and the advisory_id point lookup returns the blob intact.

The port

today becomes
new Database(p, { readonly: true, fileMustExist: true }) new DatabaseSync(p, { readOnly: true }); a missing file already throws ERR_SQLITE_ERROR, so fileMustExist has no equivalent and needs none
db.pragma("foreign_keys = ON") delete; node:sqlite enables foreign keys by default
db.transaction(fn), 2 call sites a small helper over exec("BEGIN"/"COMMIT"/"ROLLBACK"); no equivalent exists
prepare / run / get / all / exec / close identical
named parameters @id with bare object keys identical

Two things that need care

The experimental warning. node:sqlite prints ExperimentalWarning: SQLite is an experimental feature on load. The plan is to suppress that one warning and nothing else, since a user cannot act on it and it concerns which SQLite binding we use rather than their security.

A static import cannot be suppressed: builtins initialise during module linking, before any user code runs. Tested and confirmed. The working approach is a side-effecting filter module imported first, plus loading node:sqlite through a lazy createRequire at first use, which stays synchronous. When the module stabilises, both go away and a plain static import returns.

The Node floor moves from >=20 to >=22. Node 20 passed end of life in April 2026, our CI currently tests only Node 24 so the >=20 claim was never verified, and the published Action defaults adopters to 24. CI gains a Node 22 job as part of this, so the floor we declare is the floor we test.

Accepted risk

This makes us depend on an experimental core API. Mitigated by testing both supported majors in CI, recording the risk in the CHANGELOG and docs rather than in every user's terminal, and by the fact that reverting is a one-file change since better-sqlite3 reads the same file with the same SQL.

Outcome

Runtime dependencies drop from 5 to 4, and to zero native. No install scripts anywhere in the tree.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestin-houseMaintainer-handled internal work - not open for contribution

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions