Search

Comparing Encrypted SQLite Libraries for Electron — Why better-sqlite3-multiple-ciphers Is the Default Pick

Tadashi Shigeoka · Thu, August 27, 2026

Any time you build a desktop app with Electron, sooner or later you want the local SQLite database to be encrypted at rest. Credentials, customer data, drafts of business records (the usual list): you would rather not leave a plain SQLite file on disk that a stolen laptop or a sibling process can read.

The catch is that SQLite itself ships no built-in Data at Rest encryption, so you have to pick an encrypted-SQLite library separately. This article compares the candidates that matter when you are actually shipping an Electron app, along the axes that bite in production: cipher choice, availability of N-API prebuilt binaries, licensing, and how they interact with macOS signing and notarization.

Short answer for 2026: better-sqlite3-multiple-ciphers is the default pick, @signalapp/sqlcipher and the SQLite Encryption Extension (SEE) are conditional second and third picks, and @journeyapps/sqlcipher is still maintained but dropped Windows support in v6.0.0, so it is a poor fit for any Electron app that ships to Windows.

Why Encrypt the Local DB in Electron

Electron splits into a Main process (Node.js) and Renderer processes (Chromium). The Renderer can happily use IndexedDB or LocalStorage, but both land as plaintext under the user data directory. Against a stolen device, a sibling process running as the same OS user, or malware scanning the filesystem, that is essentially unprotected.

The common answer for business-grade Electron apps is to encrypt the SQLite file itself at the page level, an approach known as Transparent Data Encryption (TDE). The two mainstream implementations are SQLCipher and SQLite3 Multiple Ciphers. Both encrypt SQLite pages, so from the app’s point of view you still write ordinary SQL.

The Candidate Set

There are many SQLite / local DB libraries usable from Electron, but they split cleanly by whether they can encrypt the database.

No encryption (covered only briefly here):

  • better-sqlite3: synchronous API, N-API prebuilds, actively maintained. The default when encryption is not needed. Migrated to N-API in v13
  • sqlite3 (node-sqlite3): the traditional async binding. Repository archived and read-only as of July 2026; avoid for new work
  • node:sqlite: a built-in module in Node.js 22 and later. No native rebuild needed, but no encryption support
  • sql.js: SQLite ported to WebAssembly. Loads the full DB image into memory, which is a poor fit for large or sensitive datasets
  • lmdb-js: LMDB Node bindings. Very fast as a KV store, but no SQL and no encryption

With encryption (the focus of this article):

The rest of the article focuses on the encryption-capable set and how they compare inside an Electron project.

better-sqlite3-multiple-ciphers

better-sqlite3-multiple-ciphers is a fork of the fast, synchronous better-sqlite3 that pulls in the SQLite3 Multiple Ciphers extension. The default cipher is ChaCha20-Poly1305, with AES-256-CBC and the SQLCipher v1 to v4 profiles also available. Its author, m4heshd, is also a maintainer of upstream better-sqlite3, so the fork tracks upstream (including the N-API migration) closely.

The reasons it wins for Electron:

  • Migrated to N-API in v13.0.0. The previous model shipped separate Electron-ABI prebuilds (electron-v121, electron-v123, and so on, often close to 100 files per release); v13 ships a single N-API binary inside the npm package. Upstream better-sqlite3 states directly: “Version 13.0.0 marks a major milestone, as it’s the first version of better-sqlite3 to run on the N-API. This means prebuilt binaries should theoretically work across different versions of Node.js and Electron, and perhaps even other runtimes like Bun.” The intent is to end the “rebuild every Electron upgrade” friction
  • Prebuilds cover every mainstream platform and architecture: Windows x86 / x64 / arm64, macOS x64 / arm64, and Linux glibc / musl on x64 / arm / arm64. electron-builder and Electron Forge fetch them automatically
  • The synchronous API keeps the V8 to SQLite round-trip cheap. Upstream benchmarks put it 11.7x to 15.6x faster than node-sqlite3 (the numbers are from 2020 hardware, so treat them as directional, but the shape of the gap still holds)
  • MIT-licensed, and SQLite3 Multiple Ciphers itself is MIT, so there is no attribution obligation to display inside the app

Caveats worth naming:

  • The “theoretically” wording is directly from the upstream README. Verify on your actual targets (especially macOS arm64 universal builds and Windows arm64) that the N-API prebuild loads without rebuild
  • A database created with the default SQLite3 Multiple Ciphers format may not open in DB Browser for SQLite. Keep SQLiteStudio around for operations; it uses the same extension
  • To open a SQLCipher-format database, explicitly set db.pragma("cipher='sqlcipher'") and db.pragma("legacy=4")

Typical usage looks like this.

import Database from 'better-sqlite3-multiple-ciphers'
 
const db = new Database('app.db')
db.pragma(`key = '${passphrase}'`)
 
db.exec(`CREATE TABLE IF NOT EXISTS notes (
  id INTEGER PRIMARY KEY,
  body TEXT NOT NULL
)`)

Opening a SQLCipher-compatible file requires the extra PRAGMAs up front.

const db = new Database('legacy-sqlcipher.db')
db.pragma("cipher = 'sqlcipher'")
db.pragma("legacy = 4")
db.pragma(`key = '${passphrase}'`)

@journeyapps/sqlcipher

@journeyapps/sqlcipher has been the traditional pick: a node-sqlite3 fork bundling SQLCipher and OpenSSL. It sat at v5.3.1 (January 2022) with no updates for a long stretch, but a maintenance-oriented v6.0.0 shipped in April 2026, so the project is being maintained. What v6.0.0 actually did, however, makes it a poor fit for a cross-platform Electron app.

  • v6.0.0 dropped Windows support entirely. The release notes read: “Drop Windows support and switch the package to source-build-only installs on macOS and Linux”. A Windows-shipping Electron app cannot use it from the start
  • The same release removed node-pre-gyp and prebuilt binaries; even on macOS and Linux, installation is source-build only. Your CI and installer need a working SQLCipher / OpenSSL build chain, or npm install fails
  • On the v5 line, several projects reported issues on M1 / M2 Macs. capacitor-community is among the projects that have since migrated away from this library to better-sqlite3-multiple-ciphers
  • The old Electron Forge friction (Forge tries to rebuild from source and clashes with node-pre-gyp, forcing config.forge.electronRebuildConfig.onlyModules: []) no longer applies as-is since v6.0.0 is source-build-only, but you still own the responsibility of building against the right Electron ABI

If you are already on @journeyapps/sqlcipher, plan a migration to better-sqlite3-multiple-ciphers in SQLCipher-compatible mode (cipher='sqlcipher' + legacy=4) so you can keep the existing on-disk file format. And if your Electron app targets Windows at all, v6.0.0 is best read as “the project is still maintained, but it is walking away from your use case,” not “the project is finally getting attention.”

@signalapp/sqlcipher

@signalapp/sqlcipher is a newer N-API SQLCipher addon released by Signal Messenger in 2025. It embeds upstream SQLCipher v4.10.0 and runs in production inside Signal Desktop, so its reliability is well established.

When evaluating it for a general Electron app, licensing is the main thing to think about.

  • Licensed under AGPL-3.0-only. The README states: “Copyright 2025 Signal Messenger, LLC. Licensed under the AGPLv3”. For a closed-source commercial desktop app, AGPL’s copyleft is very likely to be a blocker; put it in front of legal before committing
  • Prebuild coverage is oriented around Signal’s own targets, so verify it on your target matrix (Windows arm64 in particular)
  • Signal-specific extensions such as their FTS5 segmentation API are bundled and typically not useful outside Signal Desktop

Reach for it only when you specifically want the upstream SQLCipher cipher (AES-256-CBC + HMAC-SHA512) with the same implementation Signal ships, and AGPL is acceptable.

SQLite Encryption Extension (SEE)

SQLite Encryption Extension is the commercial encryption extension from the SQLite team (hwaci). It supports several ciphers including AES-256-OFB, AES-128-OFB, AES-128-CCM, and AES-256-GCM, and its SQLite compatibility is essentially perfect.

The clean integration path from Electron is a custom amalgamation build via better-sqlite3’s official documentation, swapping sqlite3.c / sqlite3.h for the SEE source.

npm install better-sqlite3 \
  --build-from-source \
  --sqlite3=/path/to/see-amalgamation

Where SEE fits in the picture:

  • Licensed as a perpetual source license at US$2,000. Contrast with SQLCipher Commercial (from US$999 per app per year): the TCO shape is different, and for long-lived desktop products the fixed-cost model is attractive
  • The SEE source itself cannot be redistributed; you ship it embedded in your compiled artifact
  • The SEE team recommends AES-256-OFB for new projects, but OFB is not an authenticated cipher. If tampering with the DB file is in your threat model, choose AES-256-GCM or AES-128-CCM instead
  • SEE’s own documentation is explicit that in-memory data is not encrypted. As with SQLCipher, plaintext will be present in RAM

Realm

Realm is a C++ object database that supports file encryption with a 64-byte key. It is not SQL-compatible: you use a proprietary query API, so it does not fit if you want to bring SQL assets over as-is.

The Electron-specific things to know:

  • MongoDB announced deprecation of Atlas Device Sync and the Realm SDKs in 2024. Realm JS v20 in 2024 removed Device Sync functionality. The historical rationale of “outsource offline sync to Realm” no longer stands as of 2026
  • Realm has a Thread Affinity model: objects cannot be handed across threads. That interacts poorly with Electron’s Main / Renderer / Worker IPC model, generating frequent invalidations and serializations
  • There are GitHub issues reporting crashes when multiple Electron windows operate on the same encrypted Realm concurrently. Not a CVE, but for security-critical deployment you would want to reproduce the workload

Unless you have a concrete requirement like “our React Native mobile app already uses Realm and we want to share the domain model with the desktop version,” an encrypted SQLite library is a cleaner fit for Electron.

Comparison Table

Summarized on the axes that matter for a shipping app.

Itembetter-sqlite3-multiple-ciphers@journeyapps/sqlcipher@signalapp/sqlcipherSQLite SEERealm JS
Cipher engineSQLite3 Multiple CiphersUpstream SQLCipher + OpenSSLUpstream SQLCipher v4.10.0Upstream SQLite SEERealm Core
Default cipherChaCha20-Poly1305AES-256-CBC + HMAC-SHA512AES-256-CBC + HMAC-SHA512AES-256-OFB (recommended)AES-256
Tamper detectionDepends on cipher (Poly1305 is AEAD)Standard (HMAC-SHA512)Standard (HMAC-SHA512)Strong with GCM / CCMCustom implementation
SQL compatibilityNear-full SQLiteNear-full SQLiteNear-full SQLiteNear-full SQLiteProprietary query
APISynchronousAsync (callback)SynchronousDepends on bindingProprietary
Prebuild coverageAll mainstream platforms / archsRemoved in v6.0.0 (source-build only)Signal-orientedBuild it yourselfMainstream only
Supported platformsWindows, macOS, LinuxWindows dropped in v6.0.0 (macOS and Linux only)Signal-orientedDepends on your buildMainstream OSes
N-API supportYes (v13+)YesYesDepends on bindingYes
MaintenanceActive (v13.0.3, 2026-08)Alive again (v6.0.0, 2026-04), but Windows droppedSignal-drivenSQLite teamReduced after Sync deprecation
LicenseMITBSD-3-ClauseAGPL-3.0-onlyCommercial (US$2,000 perpetual)Apache-2.0
Closed-source commercial fitOKOKRequires legal reviewOKOK

Pitfalls Common to All Native Modules

It is tempting to file signing, notarization, and rebuild pain under “library-specific issues,” but most of it is really about Electron native modules in general. Get this layer wrong and no matter which encrypted SQLite you chose, the app fails at startup with Error: Module did not self-register or the macOS notarization service rejects it.

Unpack .node Binaries from asar

Electron’s asar archive cannot load native modules from inside. .node binaries must be extracted into app.asar.unpacked. With electron-builder, put this in package.json.

{
  "build": {
    "asarUnpack": ["**/*.node"]
  }
}

With Electron Forge, use @electron-forge/plugin-auto-unpack-natives. Recent electron-builder auto-detects native modules and unpacks them, but keep the explicit setting for safety.

macOS Hardened Runtime and Notarization

Notarization requires Hardened Runtime. With electron-builder:

{
  "build": {
    "mac": {
      "hardenedRuntime": true,
      "entitlements": "build/entitlements.mac.plist",
      "entitlementsInherit": "build/entitlements.mac.plist"
    }
  }
}

Allow JIT in the entitlements plist.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
  </dict>
</plist>

If a helper process needs to load a .node with a different Team ID, you may also need com.apple.security.cs.disable-library-validation. Load failures in this class have been reported sporadically across desktop apps in recent years, and better-sqlite3 variants are just as exposed.

The Universal Build Trap

Building a macOS universal (x64 + arm64) artifact with electron-builder splits the asar into app-x64.asar and app-arm64.asar and injects a routing index.js on top. The .app internal layout changes, and code that hardcodes assumptions about loadFile paths or the location of .node files tends to break. Shipping separate arm64 and x64 builds instead of a universal binary is a perfectly viable choice for an Electron app.

Windows Signing

.node files on Windows are individually signed with signtool. electron-builder v24 had reports of “Unrecognized file type” errors when signing Windows .node files from a Mac host via osslsigncode (issues #7652 / #7655). In v27 the Windows signing config was reorganized into win.sign as a discriminated union (signtool / hsm / pkcs11 / azure); the old win.signtoolOptions migrates automatically via migrate-schema.

Where to Store the Encryption Key

Choosing the library matters less than choosing where the passphrase or raw key lives. Hardcoding the key or writing it to a plaintext config file makes reverse engineering trivial, so those are off the table.

Electron provides the safeStorage API, which transparently persists the key into OS-specific protected storage.

On Linux, be aware that headless environments or those without a Secret Service provider fall back to plaintext storage.

The typical flow.

flowchart TB
    Init[First launch] --> Gen[Generate 256-bit master key with CSPRNG]
    Gen --> Enc[Encrypt with safeStorage.encryptString]
    Enc --> Save[Persist under user data directory]
    Boot[Subsequent launches] --> Dec[Decrypt with safeStorage.decryptString]
    Dec --> Pragma["Open DB via PRAGMA key = '...'"]
    Pragma --> Query[Handle Renderer IPC requests in Main]

The important design rule is to keep the decrypted key and the DB handle inside the Main process (or a dedicated utility process) and expose only a narrow, abstracted query API over IPC. Handing the key to the Renderer imports every XSS leak in the UI as a key leak.

safeStorage does not protect against another malicious process running as the same OS user, or against code injection into your own process. For higher-assurance use cases (financial apps, crypto wallets), pair it with a user-supplied master password fed through an OWASP-approved key derivation function (PBKDF2 at 600,000+ iterations, or Argon2id) so the key is derived at runtime.

Licensing at a Glance

License review is often the deciding factor in an enterprise procurement, so it is worth calling out the differences.

  • SQLite3 Multiple Ciphers and the better-sqlite3-multiple-ciphers binding: MIT. No attribution surface required inside the app
  • SQLCipher Community Edition: BSD-style. Commercial use is fine, but you are obliged to include the license text and Copyright (c) 2008-2026, ZETETIC, LLC in a place users can access
  • SQLCipher Commercial Edition: from US$999 per app per year. Prebuilt packages, priority support, and up to 4x faster crypto than Community. If you need FIPS 140-3, the Enterprise edition covers that
  • SQLite SEE: US$2,000 perpetual source license
  • @signalapp/sqlcipher: AGPL-3.0-only. Typically incompatible with closed-source commercial apps
  • Realm JS: Apache-2.0

What to Pick

Putting all of this together:

For the standard case (an Electron app that needs Data at Rest encryption and is a closed-source commercial product), reach for better-sqlite3-multiple-ciphers. The combination of prebuild coverage, N-API decoupling from Electron versions, active maintenance, MIT licensing, and a synchronous API with low latency is hard to beat.

For the second case (you specifically want SQLCipher as Signal ships it, and AGPL is acceptable), evaluate @signalapp/sqlcipher. Just run AGPL by legal first.

For the third case (you need commercial support from the SQLite team, or you want an explicitly authenticated cipher like AES-256-GCM), buy SQLite SEE. The perpetual license is friendly on long-lived products.

For a cross-platform Electron foundation, @journeyapps/sqlcipher and any bespoke SQLCipher stack built on node-sqlite3 fall out of the running. The former dropped Windows support and prebuilt binaries in v6.0.0 (April 2026), so any Windows-targeting Electron app cannot use it (it does remain viable for a macOS / Linux-only app, provided your CI has the source build toolchain). The latter had its repository archived in July 2026. If you depend on @journeyapps/sqlcipher today and target Windows, start planning the migration to better-sqlite3-multiple-ciphers.

Realm is worth considering only when you have a concrete requirement to share a domain model with a mobile app that already runs Realm. In every other case, the post-Sync trajectory and the Thread Affinity model make it a poor fit for Electron’s multi-process world.

Two thresholds that would change the picks:

  • If the v13 N-API prebuild of better-sqlite3-multiple-ciphers loads without rebuild on all your targets (in particular macOS arm64 with universal builds and Windows arm64), then the first-tier pick is locked in
  • If it does not load, or notarization rejects it, suspect the packaging (asar unpack of .node, entitlements including allow-jit and possibly disable-library-validation) before you suspect the library. This class of failure is usually a shared native-module issue, not something specific to the SQLite binding
  • If the Community BSD attribution or AGPL is unacceptable on business grounds, jump to the third tier (SEE)

That’s all from comparing encrypted SQLite libraries for Electron, explaining why better-sqlite3-multiple-ciphers is the default pick, and walking through the native-module signing, notarization, and key management pitfalls that apply regardless of the binding you choose, from the Gemba.

References