@graphorin/store-sqlite-encrypted
v0.15.1
Published
Optional encryption-at-rest sub-pack for the Graphorin framework's default SQLite store. Pulls in `better-sqlite3-multiple-ciphers@^12.9.0` (a drop-in fork of `better-sqlite3` that bundles SQLite3MultipleCiphers / SQLCipher v4), exposes the `encryptDataba
Maintainers
Readme
@graphorin/store-sqlite-encrypted
Optional encryption-at-rest sub-pack for the Graphorin framework's default SQLite store. Pulls in
better-sqlite3-multiple-ciphers@^12.9.0(a drop-in fork ofbetter-sqlite3that bundles the SQLite3MultipleCiphers extension) and exposes the encrypt / rekey / integrity-check runners that back thegraphorin storageCLI subcommand group.Project Graphorin · v0.15.1 · MIT License · © 2026 Oleksiy Stepurenko · https://github.com/o-stepper/graphorin
Status
- Published: v0.15.1 (optional sub-pack)
- Default cipher:
sqlcipher(SQLCipher v4 compatible,legacy=4) - Defaults: encryption-at-rest is OFF by default. Opt in through
graphorin init --encrypted. audit.db: ALWAYS encrypted regardless of this opt-in. Installing this sub-pack is the only supported way to satisfy that requirement on fresh installations.
Install
pnpm add @graphorin/store-sqlite-encrypted
# Pulls in better-sqlite3-multiple-ciphers@^12.9.0 as a peer dep.The cipher peer ships prebuilt binaries for every Node 22+ target
(macOS arm64/x64, Linux x64/arm/arm64 with both glibc and musl, Windows
x86/x64/arm64) so there is no compile step on pnpm install for the
default platforms.
If you are on a platform without a prebuilt binary you will need a C++
toolchain and Python 3 available; consult the upstream
better-sqlite3-multiple-ciphers
README for details.
pnpm 10+: approve the native build first
pnpm 10 blocks dependency install scripts by default: pnpm add exits
0, but the first real open then fails with Could not locate bindings
because the cipher peer's binding was never set up. Approve it in your
application's package.json:
{
"pnpm": {
"onlyBuiltDependencies": [
"better-sqlite3",
"better-sqlite3-multiple-ciphers"
]
}
}then run pnpm install && pnpm rebuild better-sqlite3-multiple-ciphers
(or the interactive pnpm approve-builds). pnpm scopes the approval to
each project, so every consumer needs its own copy - see the
installation guide for
the full block.
Usage
One-shot encryption migration (CLI flow)
# 1. Stop any running graphorin server / writers.
graphorin stop
# 2. Back up the unencrypted DB.
cp ~/.graphorin/data.db ~/.graphorin/data.db.backup-$(date +%Y%m%d-%H%M)
# 3. Encrypt + verify (passphrase resolved from a SecretRef chain).
graphorin storage encrypt --passphrase-from keyring:graphorin_db_passphrase
# 4. Update the config and restart.
graphorin config set storage.encryption.enabled true
graphorin config set storage.encryption.passphraseRef keyring:graphorin_db_passphrase
graphorin start
# 5. After a verification window (default 7 days) drop the backup.
graphorin storage cleanup-backups --older-than 7dProgrammatic use
import {
createEncryptedConnection,
encryptDatabase,
rekeyDatabase,
cipherIntegrityCheck,
} from '@graphorin/store-sqlite-encrypted';
// Open an existing encrypted DB.
const conn = await createEncryptedConnection({
path: '/var/lib/graphorin/data.db',
encryption: {
enabled: true,
passphraseResolver: async () => process.env.GRAPHORIN_DB_PASSPHRASE!,
},
});
// Verify the cipher header on startup or via a triggers cron.
const integrity = cipherIntegrityCheck(conn);
if (!integrity.ok) {
throw new Error(`cipher_integrity_check failed: ${integrity.rows.join('; ')}`);
}
// One-shot migration of an unencrypted file into a new encrypted one.
await encryptDatabase({
sourcePath: '/var/lib/graphorin/data.db',
targetPath: '/var/lib/graphorin/data.db.encrypted',
passphrase: process.env.GRAPHORIN_DB_PASSPHRASE!,
swap: true, // atomic rename + .bak.<timestamp> kept for recovery
});
// Rotate the passphrase in place (PRAGMA rekey under the hood).
await rekeyDatabase({
path: '/var/lib/graphorin/data.db',
oldPassphrase: process.env.OLD_PASSPHRASE!,
newPassphrase: process.env.NEW_PASSPHRASE!,
});Cipher selection
The default cipher is 'sqlcipher' with the legacy=4 parameter set -
SQLCipher v4 compatible - chosen for ecosystem tooling compatibility
(DB Browser for SQLCipher, sqlcipher CLI, GUI inspectors). Other
cipher modes shipped by the cipher peer are accepted; pass them via the
cipher option:
| Cipher | Notes |
|---------------|-----------------------------------------------------------------------------|
| 'sqlcipher' | Default. AES-256-CBC + HMAC-SHA1 + Argon2id KDF. SQLCipher v4 compatible. |
| 'chacha20' | The cipher peer's own default (ChaCha20-Poly1305). |
| 'aes256cbc' | Raw AES-256-CBC without the SQLCipher HMAC envelope. |
| 'aes128cbc' | AES-128-CBC variant. |
| 'rc4' | Legacy interop only. Do not use for new deployments. |
Operational notes
- Passphrase loss = total data loss. The cipher peer cannot
recover an encrypted DB without the passphrase. Store the passphrase
in a keyring or vault (the
graphorin storage encryptCLI prompts for this). - WAL housekeeping bytes are visible to an attacker on file leak (page numbers, lengths). Row contents are not. See ADR-030 § 5 for the threat-model nuance.
- Performance overhead is typically 5-15 % on OLTP workloads (read
/ write of small rows). The triggers cron that runs the daily
cipher_integrity_checkis a read-only pragma so it does not block writers. - Edge runtimes (Cloudflare Workers, Vercel Edge) are not
supported. The cipher peer is a native addon. For edge deployments
use
@graphorin/store-libsql(Turso encryption is a separate story).
Related decisions
- ADR-030 - SQLite encryption at rest (SQLCipher v4 baseline + KDF parameters).
- ADR-008 - Storage default
better-sqlite3(synchronous embedded SQLite + WAL hardening).
License
MIT © 2026 Oleksiy Stepurenko
Project Graphorin · v0.15.1 · MIT License · © 2026 Oleksiy Stepurenko · https://github.com/o-stepper/graphorin
