Sirannon separates the database engine from the library. You install the core package once, add the SQLite driver that matches your runtime, and use the same API everywhere. This page builds one file, db.ts, step by step. You open a database, create a table, insert parameterised rows, and read them back with a typed query. Each step highlights exactly what changed.
Install the package and a driver
Install the core package and the driver for your runtime. Node.js applications typically use better-sqlite3.
pnpm add -E @delali/sirannon-db
pnpm add -E better-sqlite3Every supported runtime has its own driver. Import the one that matches where your code runs.
| Runtime | Driver import |
|---|---|
| Node.js | @delali/sirannon-db/driver/better-sqlite3 |
| Node.js 22+ (built-in SQLite) | @delali/sirannon-db/driver/node |
| Browser (IndexedDB persistence) | @delali/sirannon-db/driver/wa-sqlite |
| Bun | @delali/sirannon-db/driver/bun |
| React Native (Expo) | @delali/sirannon-db/driver/expo |
Open a database
Create a driver, pass it to a Sirannon instance, and open a named database at a file path. The first argument is the database id you use to refer to it later, and the second is the path to the file on disk. Start db.ts here.
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
const db = await sirannon.open('tasks', './data/tasks.db')open returns a Database handle. It reads and writes through separate connection pools, keeps write-ahead logging on by default, and accepts options such as readPoolSize, walMode, and readOnly when you need them.
Create a table
With the database open, create the table your application needs. execute runs a statement that changes data or schema and returns the number of changes and the lastInsertRowId. Add a tasks table for a small task tracker.
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
const db = await sirannon.open('tasks', './data/tasks.db')
await db.execute(
'CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0)'
)CREATE TABLE IF NOT EXISTS makes the step safe to run again: the table is created once and later runs leave it untouched.
Insert parameterised rows
Never build SQL by joining strings. Pass every value through a ? placeholder so the driver binds it safely. Insert three tasks, each supplying its title and done flag as parameters.
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
const db = await sirannon.open('tasks', './data/tasks.db')
await db.execute(
'CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0)'
)
await db.execute('INSERT INTO tasks (title, done) VALUES (?, ?)', ['Draft the launch note', 0])
await db.execute('INSERT INTO tasks (title, done) VALUES (?, ?)', ['Review the pull request', 1])
await db.execute('INSERT INTO tasks (title, done) VALUES (?, ?)', ['Ship the release', 0])Three separate calls are easy to read, but inserting a longer list this way sends one statement per row. executeBatch runs the same statement across many parameter sets in a single call. Swap the three inserts for one batch.
await db.execute('INSERT INTO tasks (title, done) VALUES (?, ?)', ['Draft the launch note', 0])
await db.execute('INSERT INTO tasks (title, done) VALUES (?, ?)', ['Review the pull request', 1])
await db.execute('INSERT INTO tasks (title, done) VALUES (?, ?)', ['Ship the release', 0])
await db.executeBatch('INSERT INTO tasks (title, done) VALUES (?, ?)', [
['Draft the launch note', 0],
['Review the pull request', 1],
['Ship the release', 0],
])Each inner array is one parameter set bound to the placeholders in turn, so a single call inserts all three rows.
Run a typed query
Read the rows back. query<T> returns an array of T, and queryOne<T> returns a single row or undefined. Supply a row type so the results are typed. Fetch the tasks that are still outstanding, ordered by id.
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
const db = await sirannon.open('tasks', './data/tasks.db')
await db.execute(
'CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT NOT NULL, done INTEGER NOT NULL DEFAULT 0)'
)
await db.executeBatch('INSERT INTO tasks (title, done) VALUES (?, ?)', [
['Draft the launch note', 0],
['Review the pull request', 1],
['Ship the release', 0],
])
const outstanding = await db.query<{ id: number; title: string }>(
'SELECT id, title FROM tasks WHERE done = ? ORDER BY id',
[0]
)Placeholders bind values safely, and query returns typed rows when you supply a row type. The same call shape works inside db.transaction(async (tx) => { ... }), which commits when the callback resolves and rolls back if it throws.
Always pass user input through placeholders. Sirannon validates identifiers and rejects paths with traversal or control characters, but parameterised queries remain your first line of defence.
Grow into a networked service
The same core scales beyond an embedded file without changing your query code:
- The package's
serverexport serves databases over HTTP and WebSocket, powered by uWebSockets.js. - The
clientexport gives applications a remote database proxy with topology-aware routing. - Replication moves primary-owned changes between nodes over gRPC, and coordinator mode adds etcd-backed authority with automatic failover.
Where to go next
- The Sirannon README covers every feature area, including change data capture, migrations, backups, hooks, metrics, and the distributed replication FAQ.
- The benchmarks page documents the suite that compares Sirannon against Postgres 17 and how to reproduce it.
- The changelog lists what each release added.