Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

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 DatabaseSync connection 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.

Last updated on August 4, 2026

Was this page helpful?