Cross-platform SQL access for Node.js, web, and native runtimes with automatic adapter selection and consistent APIs.
The SQL Storage Adapter provides a single, ergonomic interface over SQLite (native and WASM), PostgreSQL, Capacitor, IndexedDB, and in-memory stores. It handles adapter discovery, capability detection, and advanced features like cloud backups so you can focus on your application logic.
π NEW in v0.6.0: SQL Dialect abstractions for SQLite/Postgres parity + Cross-platform BLOB codec + Full-text search interface!
createDatabase() inspects environment signals and picks the best backend (native SQLite, PostgreSQL, Capacitor, sql.js, IndexedDB, memory, etc.).fast, balanced, accurate, efficient presets for cost/accuracy tradeoffs. See Optimization Guide.onBeforeQuery, onAfterQuery, onBeforeWrite, onAfterWrite) for logging, analytics, caching, and custom extensions.pg, path) in browser builds.SqlDialect interface. Automatically translates INSERT OR IGNORE, json_extract, ifnull, PRAGMA between SQLite and PostgreSQL.IFullTextSearch interface abstracts FTS5 (SQLite) and tsvector/GIN (PostgreSQL) with unified createIndex, matchClause, rankExpression, and rebuildCommand APIs.IBlobCodec for cross-platform binary vector storage. NodeBlobCodec (Buffer) for server, BrowserBlobCodec (DataView) for web.IDatabaseExporter with SqliteFileExporter (VACUUM INTO) and PostgresExporter (pg_dump).# Core package
npm install @framers/sql-storage-adapter
# Install only the adapters you plan to use
npm install better-sqlite3 # Native SQLite for Node/Electron
npm install pg # PostgreSQL
npm install @capacitor-community/sqlite # Capacitor / mobile
npm install sql.js # WASM SQLite (auto-included for IndexedDB)
# IndexedDB uses sql.js (no extra install needed)
Windows users: ensure the Visual Studio Build Tools (C++ workload) are installed before adding
better-sqlite3. On Linux, installpython3,build-essential, andlibssl-devprior tonpm install.
Note: If
better-sqlite3cannot be required, install native build tools beforenpm install, ensure your Node version matches available prebuilt binaries, or fall back tosql.jsorindexeddbby settingSTORAGE_ADAPTER=sqljsorSTORAGE_ADAPTER=indexeddb.
import { createDatabase } from '@framers/sql-storage-adapter';
async function main() {
// Automatically selects the best adapter for the current runtime
const db = await createDatabase();
await db.exec(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY,
task TEXT NOT NULL,
done INTEGER DEFAULT 0
)
`);
await db.run('INSERT INTO todos (task) VALUES (?)', ['Ship cross-platform builds']);
const items = await db.all<{ id: number; task: string; done: number }>('SELECT * FROM todos');
console.log(items);
await db.close();
}
main().catch((error) => {
console.error('Database bootstrap failed', error);
process.exit(1);
});
import { createDatabase, IndexedDbAdapter } from '@framers/sql-storage-adapter';
// Web (Browser): Uses IndexedDB
const webDb = await createDatabase({ priority: ['indexeddb', 'sqljs'] });
// Desktop (Electron): Uses better-sqlite3
const desktopDb = await createDatabase({ priority: ['better-sqlite3', 'sqljs'] });
// Mobile (Capacitor): Uses native SQLite
const mobileDb = await createDatabase({ priority: ['capacitor', 'indexeddb'] });
// Cloud (Node): Uses PostgreSQL
const cloudDb = await createDatabase({
postgres: { connectionString: process.env.DATABASE_URL }
});
See Platform Strategy Guide for detailed pros/cons and architecture.
| Adapter | Package | Ideal for | Pros | Considerations |
|---|---|---|---|---|
π electron |
bundled | Electron desktop apps | IPC bridge, multi-window, WAL, auto-migrations, crash recovery | Requires Electron runtime |
π indexeddb |
bundled (sql.js) | Browsers, PWAs | sql.js + IndexedDB persistence wrapper, browser-native storage, 50MB-1GB+ quota, offline-first | IndexedDB quotas vary, WASM overhead (sql.js), not a separate SQL engine |
better-sqlite3 |
better-sqlite3 |
Node/Electron, CLI, CI | Native performance, transactional semantics, WAL support | Needs native toolchain; version must match Node ABI |
postgres |
pg |
Hosted or on-prem PostgreSQL | Connection pooling, rich SQL features, cloud friendly | Requires DATABASE_URL/credentials |
sqljs |
bundled | Browsers, serverless edge, native fallback | Pure WASM SQLite, no native deps, optional filesystem persistence | Write performance limited vs native, in-memory by default |
capacitor |
@capacitor-community/sqlite |
Mobile (iOS/Android, Capacitor) | Native SQLite on mobile, encryption | Requires Capacitor runtime |
memory |
built-in | Unit tests, storybooks, constrained sandboxes | Zero dependencies, instant startup | Non-durable, single-process only |
| Platform | Primary Adapter | Fallback | Use Case |
|---|---|---|---|
| Web (Browser) | IndexedDB | sql.js | PWAs, offline-first web apps |
| Electron (Desktop) | electron | better-sqlite3 | Desktop apps, dev tools |
| Capacitor (Mobile) | capacitor | IndexedDB | iOS/Android native apps |
| Node.js | better-sqlite3 | Postgres, sql.js | CLI tools, local servers |
| Cloud (Serverless) | Postgres | better-sqlite3 | Multi-tenant SaaS, APIs |
The Electron adapter provides a complete IPC bridge architecture for Electron apps with main/renderer process split.
// Main process (main.ts)
import { createElectronMainAdapter } from '@framers/sql-storage-adapter/electron';
const db = await createElectronMainAdapter({
filePath: path.join(app.getPath('userData'), 'app.db'),
wal: { enabled: true, checkpointInterval: 30000 },
autoMigration: { enabled: true, migrationsPath: './migrations' },
multiWindow: { enabled: true, broadcastChanges: true },
});
await db.open();
// Renderer process
import { createElectronRendererAdapter } from '@framers/sql-storage-adapter/electron';
const db = createElectronRendererAdapter();
await db.open();
const users = await db.all('SELECT * FROM users');
Features:
contextBridge APIReal-time delta synchronization across Electron, Capacitor, browser, and server platforms.
import { createCrossPlatformSync } from '@framers/sql-storage-adapter/sync';
const sync = await createCrossPlatformSync({
localAdapter: db,
endpoint: 'wss://sync.example.com',
authToken: 'bearer-token',
device: { name: 'MacBook Pro', type: 'electron' },
tables: {
notes: { priority: 'high', conflictStrategy: 'merge' },
settings: { priority: 'critical', conflictStrategy: 'local-wins' },
},
hooks: {
onConflictNeedsResolution: async (conflict) => {
// Show UI for manual conflict resolution
return showConflictDialog(conflict);
},
onSyncComplete: (result) => {
console.log(`Synced: ${result.changesPushed} pushed, ${result.changesPulled} pulled`);
},
},
});
// Manual sync
await sync.sync();
// Or enable real-time sync
await sync.connect();
Features:
last-write-wins, local-wins, remote-wins, merge, manualConflict Strategies:
| Strategy | Description |
|---|---|
last-write-wins |
Most recent change wins (by timestamp) |
local-wins |
Local changes always take priority |
remote-wins |
Remote changes always take priority |
merge |
Field-level merge with custom mergers |
manual |
Defer to UI hook for user decision |
resolveStorageAdapter inspects:
priority, type, adapter configs),STORAGE_ADAPTER, DATABASE_URL),StorageResolutionError includes the full failure chain.priority: ['indexeddb', 'sqljs'] for browser bundles or tests where native modules shouldn't load.createCloudBackupManager for S3-compatible backups with gzip compression and retention limits.import { IndexedDbAdapter } from '@framers/sql-storage-adapter';
const adapter = new IndexedDbAdapter({
dbName: 'my-app-db', // IndexedDB database name
storeName: 'sqliteDb', // Object store name
autoSave: true, // Auto-save to IndexedDB after writes
saveIntervalMs: 5000, // Batch writes every 5s
});
await adapter.open();
Key Features:
Why IndexedDB Adapter vs sql.js Adapter?
| Feature | IndexedDB Adapter | sql.js Adapter |
|---|---|---|
| SQL Engine | sql.js (WASM) | sql.js (WASM) |
| Persistence | β Automatic (saves to IndexedDB after writes) | β οΈ Manual (you must call db.export() and save yourself) |
| Data survives refresh | β Yes | β No (unless you manually saved) |
| Use Case | Production PWAs, offline-first apps | Edge functions, temporary data, prototyping |
Is IndexedDB Adapter Necessary?
β YES, if you need:
β NO, if you:
The Value: IndexedDB adapter provides automatic persistence that sql.js doesn't have. With sql.js alone, your data is lost on page refresh unless you manually export and save it. IndexedDB adapter does this automatically, making it production-ready for persistent client-side storage.
Alternative: You could use sql.js directly and manually save to IndexedDB yourself, but you'd lose:
Bottom line: IndexedDB adapter is necessary for production web apps that need persistence. For prototypes or edge functions, sql.js alone is fine.
Note: IndexedDB adapter is a wrapper around sql.js that adds IndexedDB persistence. It's not a separate SQL engineβit uses sql.js for all SQL operations and IndexedDB only for storing the database file. Since sql.js is full SQLite WASM, it supports all SQLite features including JSON functions, BLOBs, and full-text search.
See PLATFORM_STRATEGY.md for a comprehensive guide on:
TL;DR: Use IndexedDB for web, better-sqlite3 for desktop, capacitor for mobile, Postgres for cloud.
The adapter supports configurable performance tiers for different use cases:
import { createDatabase } from '@framers/sql-storage-adapter';
// Development: Fast tier - prioritize speed
const devDb = await createDatabase({
type: 'memory',
performance: { tier: 'fast' }
});
// Production: Balanced tier (default)
const prodDb = await createDatabase({
priority: ['indexeddb', 'sqljs'],
performance: { tier: 'balanced' }
});
// Analytics/Reporting: Accurate tier - no caching, full validation
const analyticsDb = await createDatabase({
postgres: { connectionString: process.env.DATABASE_URL },
performance: { tier: 'accurate', trackMetrics: true }
});
// Mobile: Efficient tier - battery optimization
const mobileDb = await createDatabase({
priority: ['capacitor', 'indexeddb'],
performance: { tier: 'efficient', batchWrites: true }
});
| Tier | Caching | Batching | Validation | Use Case |
|---|---|---|---|---|
fast |
Aggressive | Yes | Minimal | Development, testing |
balanced |
Moderate | No | Standard | General production |
accurate |
Disabled | No | Full | Analytics, reporting |
efficient |
Moderate | Yes | Minimal | Mobile, IoT |
See guides/OPTIMIZATION_GUIDE.md for detailed configuration options.
The adapter provides lifecycle hooks for extending behavior:
import { createDatabase, type StorageHooks } from '@framers/sql-storage-adapter';
const myHooks: StorageHooks = {
// Log all writes
onBeforeWrite: async (context) => {
console.log(`Write operation: ${context.statement}`);
return context;
},
// Track metrics after writes
onAfterWrite: async (context, result) => {
if (result.changes > 0) {
console.log(`Modified ${result.changes} rows`);
}
},
// Log slow queries for optimization
onAfterQuery: async (context, result) => {
const duration = Date.now() - context.startTime;
if (duration > 100) {
console.warn(`Slow query (${duration}ms):`, context.statement);
}
return result;
}
};
const db = await createDatabase({
performance: { tier: 'balanced' },
hooks: myHooks
});
| Hook | Trigger | Use Cases |
|---|---|---|
onBeforeQuery |
Before SELECT/exec | Query transformation, caching, logging |
onAfterQuery |
After successful query | Result transformation, metrics |
onBeforeWrite |
Before INSERT/UPDATE/DELETE | Validation, auditing, transformation |
onAfterWrite |
After successful write | Cache invalidation, sync triggers |
onError |
On any error | Error transformation, alerting |
See guides/OPTIMIZATION_GUIDE.md for complete configuration options.
Write cross-platform SQL that works on both SQLite and PostgreSQL without changing your application code.
import { resolveStorageAdapter, createStorageFeatures } from '@framers/sql-storage-adapter';
const adapter = await resolveStorageAdapter({ filePath: './app.db' });
const features = createStorageFeatures(adapter);
// features.dialect β SqliteDialect or PostgresDialect
// features.fts β SqliteFts5 or PostgresFts
// features.blobCodec β NodeBlobCodec or BrowserBlobCodec
// features.exporter β SqliteFileExporter or PostgresExporter
const { dialect } = features;
// INSERT OR IGNORE (SQLite) β ON CONFLICT DO NOTHING (Postgres)
const sql = dialect.insertOrIgnore('users', ['id', 'name'], ['?', '?']);
// INSERT OR REPLACE (SQLite) β ON CONFLICT DO UPDATE (Postgres)
const upsert = dialect.insertOrReplace('users', ['id', 'name'], ['?', '?'], 'id');
// json_extract(col, '$.key') (SQLite) β (col::jsonb)->>'key' (Postgres)
const expr = dialect.jsonExtract('metadata', '$.theme');
// ifnull(expr, fallback) (SQLite) β COALESCE(expr, fallback) (Postgres)
const safe = dialect.ifnull(dialect.jsonExtract('config', '$.lang'), "'en'");
// PRAGMA (SQLite) β null/no-op (Postgres)
const pragma = dialect.pragma('journal_mode', 'WAL');
if (pragma) await adapter.exec(pragma);
const { fts } = features;
// Create index: FTS5 virtual table (SQLite) or tsvector + GIN (Postgres)
await adapter.exec(fts.createIndex({
table: 'docs_fts',
columns: ['title', 'body'],
contentTable: 'documents',
tokenizer: 'porter ascii',
}));
// Search query
const sql = `
SELECT t.*
FROM ${fts.joinClause('documents', 't', 'fts', 'docs_fts')}
WHERE ${fts.matchClause('docs_fts', '?')}
ORDER BY ${fts.rankExpression('fts')}
`;
// Rebuild index
await adapter.exec(fts.rebuildCommand('docs_fts'));
const { blobCodec } = features;
// Encode a float vector for storage
const blob = blobCodec.encode([0.1, 0.2, -0.5, 1.0]);
await adapter.run('INSERT INTO embeddings (vec) VALUES (?)', [blob]);
// Decode a stored vector
const row = await adapter.get<{ vec: Uint8Array }>('SELECT vec FROM embeddings WHERE id = ?', [id]);
const vector = blobCodec.decode(row!.vec);
// Cross-platform SHA-256
const hash = await blobCodec.sha256('content to hash');
const { exporter } = features;
// Export to file (VACUUM INTO on SQLite, pg_dump on Postgres)
await exporter.exportToFile('/backups/snapshot.db');
// Export to bytes (for browser download or cloud upload)
const bytes = await exporter.exportToBytes();
On Postgres, features.exporter requires a Node runtime plus an adapter that was created with a connection string in adapter.options.connectionString, since pg_dump runs out-of-process.
ci.yml runs lint, tests, and coverage on every branch.release.yml publishes new versions to npm, tags the commit (vX.Y.Z), and creates/updates the GitHub Release when CHANGELOG.md and package.json bump the version.master.npm info @framers/sql-storage-adapter version
pnpm --filter sql-storage-adapter test
pnpm --filter sql-storage-adapter build
NPM_TOKEN), and manual fallback steps.pnpm --filter sql-storage-adapter run docs.Built and maintained by Frame.dev