Skip to main content

@stoneforge/storage

Unified SQLite storage layer that works identically across Bun, Node.js, and browser environments. Handles runtime detection, schema migrations, dirty tracking, and hierarchical ID generation.
Runtime Agnostic: Auto-detects Bun, Node.js, or browser and uses the appropriate backend.

Installation

npm install @stoneforge/storage

Runtime-Specific Dependencies

The Bun backend works out of the box. For Node.js or browser, install the corresponding optional dependency:
npm install better-sqlite3

Quick Start

import { createStorage, initializeSchema } from '@stoneforge/storage';

// Auto-detects runtime (Bun or Node)
const storage = createStorage({ path: './data.db' });

// Run migrations to set up tables
initializeSchema(storage);

// Query rows
const tasks = storage.query(
  'SELECT * FROM elements WHERE type = ?',
  ['task']
);

// Insert a row
const result = storage.run(
  'INSERT INTO elements (id, type, data, created_at, updated_at, created_by) VALUES (?, ?, ?, ?, ?, ?)',
  ['el-1', 'task', '{}', new Date().toISOString(), new Date().toISOString(), 'system']
);
console.log(result.changes); // 1

StorageBackend Interface

Every backend implements this interface. Methods are synchronous (SQLite is inherently sync); the browser backend wraps async WASM init but exposes the same sync API once initialized.

Connection Management

isOpen
boolean
Whether the connection is open
path
string
Database file path
close
() => void
Close the connection

SQL Execution

const rows = storage.query<{ id: string; title: string }>(
  'SELECT id, title FROM elements WHERE type = ?',
  ['task']
);
// rows: Array<{ id: string; title: string }>

Prepared Statements

const stmt = storage.prepare<{ id: string; title: string }>(
  'SELECT id, title FROM elements WHERE type = ? AND priority >= ?'
);

const highPriorityTasks = stmt.all('task', 3);
const oneTask = stmt.get('task', 2);

stmt.finalize(); // Clean up

Transactions

const result = storage.transaction((tx) => {
  tx.run('INSERT INTO elements (id, type) VALUES (?, ?)', ['el-1', 'task']);
  tx.run('INSERT INTO elements (id, type) VALUES (?, ?)', ['el-2', 'task']);
  return { created: 2 };
});
// Both inserts committed atomically

Schema Management

Initialize Schema

import { initializeSchema, CURRENT_SCHEMA_VERSION } from '@stoneforge/storage';

initializeSchema(storage);
console.log(`Schema at version ${CURRENT_SCHEMA_VERSION}`);

Migration System

import type { Migration } from '@stoneforge/storage';

const migration: Migration = {
  version: 1,
  name: 'initial-schema',
  up: (storage) => {
    storage.exec(`
      CREATE TABLE elements (
        id TEXT PRIMARY KEY,
        type TEXT NOT NULL,
        data TEXT NOT NULL
      );
    `);
  },
  down: (storage) => {
    storage.exec('DROP TABLE elements;');
  },
};

Schema Validation

import { validateSchema, EXPECTED_TABLES } from '@stoneforge/storage';

const isValid = validateSchema(storage);
if (!isValid) {
  console.log(`Expected tables: ${EXPECTED_TABLES.join(', ')}`);
}

Dirty Tracking

Tracks which elements have been modified in SQLite and need to be flushed to JSONL.
// Mark an element as modified
storage.markDirty('el-abc123');

Hierarchical IDs

Built-in support for generating child IDs with automatic counters.
const childNum = storage.getNextChildNumber('el-parent');
// Returns: 1 (first time), 2 (second time), etc.

const childId = `el-parent.${childNum}`;

Utilities

Statistics

const stats = storage.getStats();
console.log(stats);
// {
//   fileSize: 1048576,
//   tableCount: 8,
//   indexCount: 12,
//   schemaVersion: 1,
//   dirtyCount: 3,
//   elementCount: 150,
//   walMode: true
// }

Maintenance

const ok = storage.checkIntegrity();
if (!ok) {
  console.error('Database integrity check failed');
}

Runtime Detection

import { 
  isBunRuntime, 
  isNodeRuntime, 
  isBrowserRuntime,
  getRuntimeName 
} from '@stoneforge/storage';

if (isBunRuntime()) {
  console.log('Running in Bun');
} else if (isNodeRuntime()) {
  console.log('Running in Node.js');
} else if (isBrowserRuntime()) {
  console.log('Running in browser');
}

const runtime = getRuntimeName(); // 'bun' | 'node' | 'browser' | 'unknown'

Configuration

Storage Config

path
string
required
Database file path
create
boolean
default:"true"
Create database if it doesn’t exist
readonly
boolean
default:"false"
Open in read-only mode
verbose
boolean
default:"false"
Log SQL statements to console
pragmas
SqlitePragmas
Custom SQLite pragmas (journal mode, cache size, etc.)

SQLite Pragmas

import { DEFAULT_PRAGMAS } from '@stoneforge/storage';

console.log(DEFAULT_PRAGMAS);
// {
//   journal_mode: 'WAL',
//   synchronous: 'NORMAL',
//   cache_size: -64000,
//   foreign_keys: 'ON',
//   temp_store: 'MEMORY'
// }

Error Handling

Error Utilities

import { mapStorageError, isConstraintError } from '@stoneforge/storage';

try {
  storage.run('INSERT INTO elements ...');
} catch (error) {
  const storageError = mapStorageError(error, 'insert');
  
  if (isConstraintError(storageError)) {
    console.error('Constraint violation');
  }
}

Entry Points

import { createStorage, initializeSchema } from '@stoneforge/storage';
// Auto-detects Bun or Node, exports types, errors, schema utilities

API Reference

Factory Functions

FunctionSignatureDescription
createStorage(config: StorageConfig) => StorageBackendSync factory (Bun/Node only)
createStorageAsync(config: StorageConfig) => Promise<StorageBackend>Async factory (all runtimes)

Schema Functions

FunctionSignatureDescription
initializeSchema(backend: StorageBackend) => voidRun all pending migrations
getSchemaVersion(backend: StorageBackend) => numberGet current version
isSchemaUpToDate(backend: StorageBackend) => booleanCheck if at latest version
getPendingMigrations(backend: StorageBackend) => Migration[]Get unapplied migrations
validateSchema(backend: StorageBackend) => booleanCheck all expected tables exist
resetSchema(backend: StorageBackend) => voidDrop all tables (testing only)

Constants

ConstantTypeValue
CURRENT_SCHEMA_VERSIONnumberLatest schema version
EXPECTED_TABLESstring[]Table names after full migration
DEFAULT_PRAGMASSqlitePragmasDefault SQLite pragmas
Performance: SQLite with WAL mode is extremely fast for read-heavy workloads. The cache can hold the entire database in memory for small projects.

Next Steps

Core Types

Understand the data model

Quarry API

Use the high-level API built on storage

Build docs developers (and LLMs) love