Getting started

Getting started

Install Sirannon, choose the driver for your runtime, open a database, create a table, insert parameterised rows, and run a typed query.

Table of Contents

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 will open a database, create a table, insert parameterised rows, and read them back with a typed query. Every step marks the lines that 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-sqlite3

Every supported runtime has its own driver. Import the one that matches where your code runs.

RuntimeDriver 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

The same API works on all five. Three of its features, however, depend on what the SQLite engine underneath can do, and driver.capabilities reports all three at start-up as multipleConnections, extensions, and steppedCopy. Once a database is open, db.backupCapabilities() answers the backup question in more detail.

DrivermultipleConnectionsextensions (guide)steppedCopy (guide)
better-sqlite3YesYesYes
nodeYesYesYes, on a build whose node:sqlite exports a backup function
bunYesYesNo
wa-sqliteNoNoNo
expoNoNoNo

A driver that reports multipleConnections: false sends every read through the writer under the writer lock. A driver that reports steppedCopy: false fails db.backup with BACKUP_UNSUPPORTED, so a browser database and a device database take their copies through the device sync snapshot.

Sirannon writes the database file into a directory you create yourself, so make one before you run any of the code below.

mkdir -p data

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.

db.ts
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 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 uses. 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.

db.ts
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, because SQLite creates the table once and later runs leave it untouched.

Insert parameterised rows

Build every statement from a fixed string, and pass every value through a ? placeholder so that the driver binds it safely. Insert three tasks, each supplying its title and done flag as parameters.

db.ts
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.

db.ts
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])
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 that the results are typed. Fetch the tasks that are still outstanding, ordered by id.

db.ts
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 server export serves databases over HTTP and WebSocket, powered by uWebSockets.js. It runs the reads and writes you register, and a caller sends no SQL.
  • The client export gives applications a remote database proxy, and client/topology adds routing across the nodes of a replication group.
  • The react export turns a live query into a hook, and codegen emits typed references from your registry.
  • 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.