Overview · File · SQLite · MySQL · Migration · Encryption · Which One?
NUTbits supports three storage backends. Choose based on your use case.
file |
sqlite |
mysql |
|
|---|---|---|---|
| Best for | Personal use, simple setups | LNbits, concurrent payments | A shared/remote database |
| Concurrent safety | Write mutex (serialized) | Full ACID transactions | Full ACID transactions |
| Multiple instances | Not supported | Not supported | Not supported (see below) |
| Dependencies | None | better-sqlite3 |
mysql2 |
| Setup | Zero config | Zero config | Requires MySQL server |
| Portable | Single .enc file |
Single .db file |
Remote database |
| Encryption | Whole-file AES-256-GCM | Per-column AES-256-GCM | Per-column AES-256-GCM |
The original approach. Entire state is encrypted and written to a single .enc file.
# .env
NUTBITS_STATE_BACKEND=file
NUTBITS_STATE_FILE=./nutbits_state.enc
NUTBITS_STATE_PASSPHRASE=your-passphraseHow it works: All state lives in memory. On every mutation, the entire state is encrypted and written atomically (write to .tmp, then rename). A write mutex prevents concurrent writes from clobbering each other.
Limitation: Two concurrent payments can still read stale proof data between async await points. The mutex serializes writes but not reads. For single-user setups this rarely matters. For LNbits with concurrent users, use SQLite or MySQL.
See STATE.md for encryption details and manual decryption.
Atomic per-proof operations via SQLite transactions. Best balance of simplicity and safety.
# Install driver
npm install better-sqlite3
# .env
NUTBITS_STATE_BACKEND=sqlite
NUTBITS_STATE_PASSPHRASE=your-passphrase
# Optional: custom path (default: nutbits_state.db)
# NUTBITS_SQLITE_PATH=./nutbits.dbHow it works: Each proof, connection, and transaction is a row in the database. Sensitive fields (private keys, proof secrets, invoices) are encrypted per-column with AES-256-GCM. Proof operations use SQLite transactions for atomicity; two concurrent payments cannot select the same proofs.
Schema:
proofs - one row per ecash proof (proof_id, mint_url, amount, proof_enc)
connections - one row per NWC connection (app_pubkey, data_enc, balance)
includes per-connection: permissions, spending limits, service fee rates
transactions - one row per payment (payment_hash, app_pubkey, data_enc)
includes: fees_paid (routing), service_fee (operator), settled_at
daily_spend - per-connection daily spend tracking (spend_key, sats)
reservations - funds committed to a payment in flight (id, app_pubkey, gross_msat,
net_msat, created_at, held, reason) - owned by ledger.js, removed on
settle/release/resolve, restored as holds at boot. Not encrypted:
no secrets, same as balance and daily_spend.
mints - configured mints (url, last_healthy)
config - key-value settings (active_mint_url, encryption_salt, schema_version)
Existing databases upgrade themselves on first start. CREATE TABLE IF NOT EXISTS runs on every open, so new tables simply appear; the schema_version
row in config then moves forward in its own transaction. Nothing is rewritten
and nothing is lost.
For a shared or remote database - typically because you already run MySQL, or want the wallet state to live somewhere other than the NUTbits host.
One instance at a time, on any backend.
MySQL gives you a remote database, not a clustered NUTbits. Two NUTbits processes against one database will spend the same ecash twice: proof selection and the allocation ledger are both serialized by in-process locks, so neither instance can see the other's in-flight work. Both read the same proofs, both melt them, and one payment fails while its proofs are restored as though unspent.
Since v0.8.1 a second instance on the same host is refused at boot via a PID lock beside the state file. That guard cannot see another machine, so pointing two hosts at one MySQL database is still your responsibility to avoid. Payment reservations are now persisted, which was one prerequisite, but the remaining one is not met: the locks in
lib/mutex.jsserialize one event loop, not a cluster, so two processes still cannot see each other's in-flight work. Making it safe needs a lock held in the database itself.
# Install driver
npm install mysql2
# .env
NUTBITS_STATE_BACKEND=mysql
NUTBITS_STATE_PASSPHRASE=your-passphrase
NUTBITS_MYSQL_URL=mysql://user:password@localhost:3306/nutbitsHow it works: Same schema and encryption as SQLite, adapted for MySQL. Uses connection pooling and InnoDB transactions. Tables are auto-created on first run.
Create the database first:
CREATE DATABASE nutbits CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'nutbits'@'%' IDENTIFIED BY 'your-db-password';
GRANT ALL PRIVILEGES ON nutbits.* TO 'nutbits'@'%';Automatic. On first run with a new backend, NUTbits detects the existing .enc file and migrates all data:
- Decrypts the
.encfile - Inserts all proofs, connections, and transactions into the database
- Renames the
.encfile to.enc.migrated(backup)
[MIGRATION] Migrated .enc state to SQLite. Old file renamed to nutbits_state.enc.migrated
No manual steps needed. Just change NUTBITS_STATE_BACKEND and restart.
Not automatic. Export/import manually:
- Stop NUTbits
- Use the old backend to read state
- Use the new backend to write state
- Update
.envand restart
All backends encrypt sensitive data with the same algorithm:
- Key derivation: scrypt (passphrase + random salt = 32-byte AES key)
- Encryption: AES-256-GCM (authenticated encryption)
- Per-value IV: Each encrypted field gets a fresh 12-byte random IV
- Format:
[IV (12 bytes)][Auth Tag (16 bytes)][Ciphertext]
The encryption salt is stored:
- File backend: Random per-save (embedded in the
.encfile header) - SQLite/MySQL: Generated once at database creation, stored in the
configtable
The NUTBITS_STATE_PASSPHRASE is the only secret. Without it, the data is unreadable.
Personal use, single user → file
LNbits funding source → sqlite
State must live off-host → mysql
Multiple NUTbits instances → not supported on any backend
- STATE.md - encrypted state file format and manual decryption
- BACKUP.md - backup, recovery, and seed recovery
- INSTALL.md - getting NUTbits up and running