Comparing Encrypted SQLite Libraries for Electron — Why better-sqlite3-multiple-ciphers Is the Default Pick
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 v13sqlite3(node-sqlite3): the traditional async binding. Repository archived and read-only as of July 2026; avoid for new worknode:sqlite: a built-in module in Node.js 22 and later. No native rebuild needed, but no encryption supportsql.js: SQLite ported to WebAssembly. Loads the full DB image into memory, which is a poor fit for large or sensitive datasetslmdb-js: LMDB Node bindings. Very fast as a KV store, but no SQL and no encryption
With encryption (the focus of this article):
better-sqlite3-multiple-ciphers: a fork of better-sqlite3 with SQLite3 Multiple Ciphers built in. Also supports a SQLCipher-compatible mode@journeyapps/sqlcipher: a fork of node-sqlite3 bundling upstream SQLCipher and OpenSSL@signalapp/sqlcipher: a newer N-API SQLCipher addon published by Signal Messenger in 2025- SQLite Encryption Extension (SEE): the official commercial encryption extension from the SQLite team
- Realm: not SQLite, but often included in the comparison because it supports encrypted local storage
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'")anddb.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 installfails - 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-amalgamationWhere 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.
| Item | better-sqlite3-multiple-ciphers | @journeyapps/sqlcipher | @signalapp/sqlcipher | SQLite SEE | Realm JS |
|---|---|---|---|---|---|
| Cipher engine | SQLite3 Multiple Ciphers | Upstream SQLCipher + OpenSSL | Upstream SQLCipher v4.10.0 | Upstream SQLite SEE | Realm Core |
| Default cipher | ChaCha20-Poly1305 | AES-256-CBC + HMAC-SHA512 | AES-256-CBC + HMAC-SHA512 | AES-256-OFB (recommended) | AES-256 |
| Tamper detection | Depends on cipher (Poly1305 is AEAD) | Standard (HMAC-SHA512) | Standard (HMAC-SHA512) | Strong with GCM / CCM | Custom implementation |
| SQL compatibility | Near-full SQLite | Near-full SQLite | Near-full SQLite | Near-full SQLite | Proprietary query |
| API | Synchronous | Async (callback) | Synchronous | Depends on binding | Proprietary |
| Prebuild coverage | All mainstream platforms / archs | Removed in v6.0.0 (source-build only) | Signal-oriented | Build it yourself | Mainstream only |
| Supported platforms | Windows, macOS, Linux | Windows dropped in v6.0.0 (macOS and Linux only) | Signal-oriented | Depends on your build | Mainstream OSes |
| N-API support | Yes (v13+) | Yes | Yes | Depends on binding | Yes |
| Maintenance | Active (v13.0.3, 2026-08) | Alive again (v6.0.0, 2026-04), but Windows dropped | Signal-driven | SQLite team | Reduced after Sync deprecation |
| License | MIT | BSD-3-Clause | AGPL-3.0-only | Commercial (US$2,000 perpetual) | Apache-2.0 |
| Closed-source commercial fit | OK | OK | Requires legal review | OK | OK |
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.
- macOS: uses Keychain
- Windows: uses DPAPI (Data Protection API)
- Linux: uses the Secret Service API via GNOME Keyring or KWallet
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-ciphersbinding: 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, LLCin 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-ciphersloads 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 includingallow-jitand possiblydisable-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
- better-sqlite3-multiple-ciphers
- SQLite3 Multiple Ciphers documentation
- WiseLibs/better-sqlite3
- SQLCipher (Zetetic)
- SQLite Encryption Extension (SEE)
- @signalapp/sqlcipher
- @journeyapps/sqlcipher (node-sqlcipher repository)
- Electron safeStorage API
- Electron asar archives
- Notarizing macOS software before distribution
- OWASP Password Storage Cheat Sheet