SQLite
Configure Ownfold's built-in Node.js SQLite adapter, initialize its strict schema, and choose a safe production deployment topology.
@ownfold/sqlite uses Node.js’s built-in node:sqlite module. It is a good fit for single-process
applications, local-first servers, desktop companions, tests, and modest vault-coordination traffic.
It is not the recommended adapter for horizontally scaled web deployments.
Requirements
- Node.js 22.13 or newer;
- one process writing to the database file; and
- a dedicated
DatabaseSyncconnection for Ownfold operations.
The package does not add a third-party native SQLite dependency.
Install
pnpm add @ownfold/sqlite @ownfold/server
Create the database
Generate and apply the schema with the CLI:
pnpm add -D @ownfold/cli
pnpm ownfold init --database sqlite --identity custom
pnpm ownfold migrate --database-path ./ownfold.sqlite
Then create the adapter. If application startup owns schema initialization, the exported helper is the programmatic equivalent:
import { DatabaseSync } from "node:sqlite"
import { applyOwnfoldSqliteSchema, SqliteVaultAdapter } from "@ownfold/sqlite"
export const database = new DatabaseSync("ownfold.sqlite")
database.exec(`
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
`)
applyOwnfoldSqliteSchema(database)
export const vaultAdapter = new SqliteVaultAdapter(database)
applyOwnfoldSqliteSchema is idempotent. Call it during a controlled startup or migration phase,
before the HTTP server begins accepting requests.
Connect the server
import { createVaultServer } from "@ownfold/server"
import { vaultAdapter } from "./database"
export const vaultServer = createVaultServer({
adapter: vaultAdapter,
getUserId: async ({ request }) => {
const session = await readApplicationSession(request)
return session?.user.id ?? null
},
})
Transaction behavior
Writes use BEGIN IMMEDIATE, which obtains the write lock before reading the revisions needed for
the update. Rotation cutover validates the complete active-device replacement set and then updates
the device envelopes, vault key version, Recovery Kit ID, and rotation state in one transaction.
If validation fails, the adapter rolls back and preserves the last committed state. Do not wrap an adapter operation in another transaction on the same connection; SQLite does not provide nested transactions for this usage.
Stored schema
The schema creates strict vault, device, pairing, and rotation tables with ownership indexes and
foreign keys. Persisted JSON is parsed through Ownfold’s versioned core schemas on every read.
Malformed rows return StorageAdapterError and never flow into server operations as trusted data.
The database does not contain:
- record plaintext;
- the root vault key;
- device private keys;
- recovery passwords or codes; or
- a maintainer-controlled recovery key.
Deployment topology
Recommended. Keep one process-wide connection, enable WAL, and close it during graceful shutdown.
Do not place a normal SQLite file on ephemeral or instance-local storage when multiple functions must share vault state. Use a supported PostgreSQL deployment instead.
Do not mount one SQLite file over a network filesystem and assume it behaves like PostgreSQL. Use the PostgreSQL, Drizzle, or Prisma adapter for horizontal scaling.
Backups
Use SQLite’s supported backup facilities or stop writes while copying a consistent database. With WAL enabled, copying only the main file while writes continue can omit committed WAL data. Test a full restore and then run an Ownfold read flow against the restored copy.
Backups still contain sensitive encrypted envelopes and metadata, including device labels and activity timestamps. Apply normal access controls and retention limits.
Test the adapter
import { checkVaultRotationAdapterCompliance } from "@ownfold/testing"
const result = await checkVaultRotationAdapterCompliance(vaultAdapter)
if (result.status === "error") throw result.error
Run the suite against a temporary database, never a production file. It writes fixed test records and deliberately creates stale revisions to verify rollback behavior.
Troubleshooting
database is locked
Confirm busy_timeout is configured, all operations use the same intended connection, and no long
external transaction holds the writer lock. Persistent lock contention means the deployment has
outgrown the single-writer topology.
cannot start a transaction within a transaction
An adapter call was made inside another transaction on the same DatabaseSync connection. Remove
the outer transaction; the adapter owns the atomic boundary it needs.
Stored data fails validation
The row is malformed or was modified outside the adapter. Preserve the database for diagnosis and restore a known-good backup if necessary. Do not coerce malformed encrypted envelopes into a success-shaped result.
Requests become slow under load
DatabaseSync performs synchronous work. Measure event-loop delay and move to PostgreSQL when
coordination writes are no longer short and infrequent.
Production checklist
- Use Node.js 22.13 or newer.
- Enable foreign keys, WAL, and a finite busy timeout.
- Use one process-wide, dedicated connection.
- Keep the file on reliable local persistent storage.
- Back up the main database and WAL consistently.
- Test restoration.
- Never share the file between horizontally scaled replicas.
- Move to PostgreSQL before write contention affects vault lifecycle operations.