
Security News
CVE Volume Surges Past 48,000 in 2025 as WordPress Plugin Ecosystem Drives Growth
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.
@loro-dev/unisqlite
Advanced tools
Universal SQLite adapter for Node.js, Cloudflare Workers, and browsers.
getRole() / subscribeRoleChange() so apps can avoid redundant host-only workimport { openStore } from "@loro-dev/unisqlite";
const store = await openStore({ path: "my-database.db" });
// Basic operations
await store.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
await store.run("INSERT INTO users (name) VALUES (?)", ["Alice"]);
const users = await store.query("SELECT * FROM users");
// Transactions
await store.transaction(async (txn) => {
await txn.run("INSERT INTO users (name) VALUES (?)", ["Bob"]);
await txn.run("INSERT INTO users (name) VALUES (?)", ["Charlie"]);
});
await store.close();
For Cloudflare Durable Objects with the new SQLite persistence API:
import { CloudflareDOAdapter, createCloudflareDOAdapter } from "@loro-dev/unisqlite";
export class MyDurableObject extends DurableObject {
private db: CloudflareDOAdapter;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Use the factory function for easy setup
this.db = createCloudflareDOAdapter(ctx.storage.sql);
}
async fetch(request: Request) {
// Use the adapter like any other UniSQLite connection
await this.db.run("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)");
await this.db.run("INSERT INTO users (name) VALUES (?)", ["Alice"]);
const users = await this.db.query("SELECT * FROM users");
return Response.json({ users });
}
}
UniSQLite provides three types of connections with different transaction capabilities:
"direct")transaction() and asyncTransaction()const store = await openStore({ path: "database.db" });
console.log(store.getConnectionType()); // "direct"
// Both transaction types are supported
await store.transaction(async (txn) => {
/* sync transaction */
});
await store.asyncTransaction(async (txn) => {
/* async transaction */
});
"syncTxn")transaction() callstransaction() using itself (for nested operations)asyncTransaction() - will throw an errorawait store.transaction(async (txn) => {
console.log(txn.getConnectionType()); // "syncTxn"
// ✅ Allowed: nested transaction using same connection
await txn.transaction(async (nested) => {
// nested === txn (same connection)
await nested.run("INSERT INTO users (name) VALUES (?)", ["user"]);
});
// ❌ Not allowed: will throw error
await txn.asyncTransaction(async (nested) => {
// Error: "asyncTransaction is not supported in syncTxn connections"
});
});
"asyncTxn")asyncTransaction() callstransaction() and asyncTransaction() using itselfawait store.asyncTransaction(
async (txn) => {
console.log(txn.getConnectionType()); // "asyncTxn"
// ✅ Both transaction types are supported
await txn.transaction(async (nested) => {
// nested === txn (same connection)
});
await txn.asyncTransaction(async (nested) => {
// nested === txn (same connection)
});
},
{ timeoutMs: 30000 }
);
The browser adapter supports multiple storage backends for persistence. By default, it uses 'auto' mode which tries each backend in order until one succeeds.
| Backend | Description | Requirements |
|---|---|---|
'opfs' | Origin Private File System - best for persistent storage with custom database names | Secure context (HTTPS/localhost), modern browser |
'localStorage' | localStorage-backed storage | Path must be 'local' or 'session' only |
'memory' | In-memory storage (no persistence) | None |
'auto' | Try OPFS → localStorage → memory (default) | None |
If you set storageBackend: 'opfs', UniSQLite will not silently fall back — openStore() will throw if OPFS cannot be opened. Use storageBackend: 'auto' if you want automatic fallback.
OPFS is ideal for multi-workspace applications that need separate persistent databases:
import { openStore } from "@loro-dev/unisqlite";
// Each workspace gets its own persistent database
const db = await openStore({
path: `workspace-${workspaceId}.db`,
sqlite: {
loadStrategy: 'cdn',
storageBackend: 'opfs' // Use OPFS for custom database names
}
});
// Check which storage backend is active
const info = db.getSQLiteInfo();
console.log('Storage backend:', info.activeStorageBackend); // 'opfs'
// Or use the dedicated method
console.log('Storage backend:', db.getStorageBackend()); // 'opfs'
In 'auto' mode, the adapter tries backends in order and falls back gracefully:
const db = await openStore({
path: 'my-database.db',
sqlite: {
storageBackend: 'auto' // Default - tries OPFS first
}
});
// Will use OPFS if available, otherwise falls back
console.log('Using:', db.getStorageBackend()); // 'opfs', 'localStorage', or 'memory'
SQLite WASM supports multiple OPFS-backed VFS implementations. Configure via sqlite.opfsVfsType:
const db = await openStore({
path: 'my-database.db',
sqlite: {
storageBackend: 'opfs',
opfsVfsType: 'auto', // 'auto' (default) | 'opfs' | 'sahpool'
}
});
Note: SQLite WASM OPFS backends are worker-only. When UniSQLite is used from the main thread with
storageBackend: 'opfs', it will transparently use SQLite WASM's wrapped-worker API under the hood
(i.e. it will start a Worker). There is currently no separate "allow worker" option — requesting
OPFS implies Worker usage.
sqlite.loadStrategy must be able to load sqlite3Worker1Promiser (use 'npm' or 'cdn', or provide it globally).'opfs': Requires COOP/COEP headers (window.crossOriginIsolated === true) so SharedArrayBuffer is available.The localStorage backend only accepts 'local' or 'session' as database paths:
// ✅ Works with localStorage
const db = await openStore({
path: 'local', // or 'session'
sqlite: { storageBackend: 'localStorage' }
});
// ❌ Throws error - invalid path for localStorage
const db = await openStore({
path: 'my-custom-db.db',
sqlite: { storageBackend: 'localStorage' }
});
// Error: localStorage storage backend requires path to be 'local' or 'session'
When using bundlers in production, the default CDN-based Worker loading may not be ideal. UniSQLite provides a workerUrl option for proper bundler integration.
By default, UniSQLite creates Workers dynamically using Blob URLs that load SQLite WASM from a CDN. This works for development but has production drawbacks:
| Default (CDN) | Custom workerUrl |
|---|---|
| ❌ Requires runtime CDN access | ✅ Fully bundled, works offline |
| ❌ Bypasses bundler optimizations | ✅ Tree-shaking, minification |
| ❌ May violate CSP | ✅ CSP compliant |
| ❌ Version mismatch risk | ✅ Consistent versions |
Step 1: Create a Worker file
// src/sqlite-worker.ts
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
sqlite3InitModule().then(async (sqlite3) => {
// Optional: Install SAHPool VFS (no COOP/COEP required)
if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') {
try {
await sqlite3.installOpfsSAHPoolVfs({
name: 'opfs-sahpool',
directory: '/unisqlite-sahpool',
});
} catch (e) {
console.warn('SAHPool VFS installation failed:', e);
}
}
// Initialize Worker API
sqlite3.initWorker1API();
});
Step 2: Import and use the Worker URL
// src/db.ts
import { openStore } from '@loro-dev/unisqlite';
// Vite processes this import and bundles the worker
import sqliteWorkerUrl from './sqlite-worker?worker&url';
export async function createDatabase(name: string) {
return await openStore({
name,
sqlite: {
workerUrl: sqliteWorkerUrl,
storageBackend: 'opfs',
},
});
}
Step 3: Configure Vite (if needed)
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
optimizeDeps: {
exclude: ['@sqlite.org/sqlite-wasm'],
},
worker: {
format: 'es',
},
});
Step 1: Create a Worker file
// src/sqlite-worker.js
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
sqlite3InitModule().then(async (sqlite3) => {
// Optional: Install SAHPool VFS
if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') {
try {
await sqlite3.installOpfsSAHPoolVfs({
name: 'opfs-sahpool',
directory: '/unisqlite-sahpool',
});
} catch (e) {
console.warn('SAHPool VFS installation failed:', e);
}
}
sqlite3.initWorker1API();
});
Step 2: Import and use the Worker URL
// src/db.ts
import { openStore } from '@loro-dev/unisqlite';
// Webpack 5 processes this with asset modules
const sqliteWorkerUrl = new URL('./sqlite-worker.js', import.meta.url);
export async function createDatabase(name: string) {
return await openStore({
name,
sqlite: {
workerUrl: sqliteWorkerUrl.href,
storageBackend: 'opfs',
},
});
}
If TypeScript complains about the ?worker&url import, add this to your declarations:
// src/vite-env.d.ts or src/global.d.ts
declare module '*?worker&url' {
const workerUrl: string;
export default workerUrl;
}
When creating your Worker, you can choose which VFS to install:
| VFS | Requirements | When to Use |
|---|---|---|
opfs (default) | COOP/COEP headers | Best performance, requires SharedArrayBuffer |
opfs-sahpool | None | No special headers needed, works everywhere |
For SAHPool (recommended for wider compatibility):
// sqlite-worker.ts
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
sqlite3InitModule().then(async (sqlite3) => {
// Install SAHPool - works without COOP/COEP
if (typeof sqlite3.installOpfsSAHPoolVfs === 'function') {
await sqlite3.installOpfsSAHPoolVfs({
name: 'opfs-sahpool',
directory: '/unisqlite-sahpool',
});
}
sqlite3.initWorker1API();
});
Then configure UniSQLite to use SAHPool:
const db = await openStore({
name: 'mydb',
sqlite: {
workerUrl: sqliteWorkerUrl,
storageBackend: 'opfs',
opfsVfsType: 'sahpool', // Use SAHPool VFS
},
});
If workerUrl is not provided, UniSQLite falls back to CDN-based Worker creation. This is useful for:
// Development: no workerUrl needed
const db = await openStore({
name: 'mydb',
sqlite: {
loadStrategy: 'cdn', // Load from CDN
storageBackend: 'opfs',
},
});
The default unisqlite export stays minimal and uses dynamic imports so optional peers are only loaded when their platform adapter is actually needed. To make bundling and installations more intentional, use platform-specific entrypoints:
better-sqlite3): import { openNodeStore, NodeAdapter } from "@loro-dev/unisqlite/node";broadcast-channel, and @sqlite.org/sqlite-wasm if you load SQLite from npm): import { openStore } from "@loro-dev/unisqlite/browser";import { openCloudflareStore, CloudflareDOAdapter, createCloudflareDOAdapter } from "@loro-dev/unisqlite/cloudflare";These entrypoints avoid pulling in other platform adapters, keeping browser bundles slim and Node installs free from WASM/downloaded assets.
In browsers, UniSQLite elects a single writable host tab for a given database. SQL calls are already transparently forwarded through RPC, but applications may want to run background tasks (sync, persistence/flush, etc.) only on the host tab.
The browser adapter exposes a lightweight, capability-based role API:
getRole(): "host" | "participant" | "unknown"subscribeRoleChange(listener): () => voidwaitForRole(role, timeoutMs?): Promise<void>These methods are optional on UniStoreConnection for compatibility with other adapters/older versions, so feature-detect them:
import { openStore } from "@loro-dev/unisqlite/browser";
const db = await openStore({ path: "my.db" });
if (db.getRole?.() === "host") {
// Start background sync/persistence only on the host tab.
}
const unsubscribe = db.subscribeRoleChange?.((role) => {
if (role === "host") {
// Became host.
} else if (role === "participant") {
// Lost host.
} else {
// "unknown": initializing/closing; treat conservatively.
}
});
Role semantics:
host means host-ready (safe to run host-only work)participant means initialized but not hostunknown is a transitional state during initialization/close; treat as participantUniSQLite browser logs are gated by localStorage.debug (errors always log). Examples:
// All UniSQLite logs
localStorage.debug = "unisqlite:*";
// Only adapter + host election logs
localStorage.debug = "unisqlite:adapter,unisqlite:host";
interface UniStoreConnection {
getConnectionType(): "direct" | "syncTxn" | "asyncTxn";
// Optional (browser multi-tab only)
getRole?: () => "host" | "participant" | "unknown";
subscribeRoleChange?: (listener: (role: "host" | "participant" | "unknown") => void) => () => void;
waitForRole?: (role: "host" | "participant" | "unknown", timeoutMs?: number) => Promise<void>;
// ... other methods
}
// Synchronous transaction - function cannot return Promise
transaction<T>(fn: (tx: UniStoreConnection) => T extends Promise<unknown> ? never : T): T extends Promise<unknown> ? never : T;
// Asynchronous transaction - function can return Promise
asyncTransaction<T>(fn: (tx: UniStoreConnection) => Promise<T>, options?: { timeoutMs?: number }): Promise<T>;
| Connection Type | transaction() | asyncTransaction() |
|---|---|---|
"direct" | ✅ Supported | ✅ Supported |
"syncTxn" | ✅ Supported | ❌ Throws Error |
"asyncTxn" | ✅ Supported | ✅ Supported |
The transaction() method enforces synchronous functions at compile time:
// ✅ Valid - returns non-Promise value
const result = store.transaction((txn) => {
return "sync-result";
});
// ❌ TypeScript Error - returns Promise
const invalid = store.transaction((txn) => {
return Promise.resolve("async-result"); // Type error!
});
// ❌ TypeScript Error - async function
const invalid2 = store.transaction(async (txn) => {
return "result"; // Type error!
});
This design ensures:
FAQs
Cross-platform concurrent SQLite access layer
We found that @loro-dev/unisqlite demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
CVE disclosures hit a record 48,185 in 2025, driven largely by vulnerabilities in third-party WordPress plugins.

Security News
Socket CEO Feross Aboukhadijeh joins Insecure Agents to discuss CVE remediation and why supply chain attacks require a different security approach.

Security News
Tailwind Labs laid off 75% of its engineering team after revenue dropped 80%, as LLMs redirect traffic away from documentation where developers discover paid products.