> Complete content index: https://sirannon.sondelali.com/llms.txt

# Getting started

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

URL: https://sirannon.sondelali.com/docs/getting-started
Section: Getting started
Version: 0.3 (@delali/sirannon-db@0.3.0, latest)

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`.

```bash pm
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.

| 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` |

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.

| Driver | `multipleConnections` | `extensions` ([guide](/docs/extensions)) | `steppedCopy` ([guide](/docs/backups)) |
| --- | --- | --- | --- |
| `better-sqlite3` | Yes | Yes | Yes |
| `node` | Yes | Yes | Yes, on a build whose `node:sqlite` exports a `backup` function |
| `bun` | Yes | Yes | No |
| `wa-sqlite` | No | No | No |
| `expo` | No | No | No |

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](/docs/device-sync-recovery).

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

```bash
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.

```ts title="db.ts"

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 <Tooltip tip="Write-ahead logging makes SQLite append each change to a separate log file before updating the main database file, so a crash leaves committed writes intact, and reads can run alongside a write.">write-ahead logging</Tooltip> 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.

```ts title="db.ts"

const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
const db = await sirannon.open('tasks', './data/tasks.db')
// [!code ++:4]

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.

```ts title="db.ts"

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)'
)
// [!code ++:4]

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.

```ts title="db.ts"

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)'
)

// [!code --:3]
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])
// [!code ++:5]
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.

```ts title="db.ts"

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],
])
// [!code ++:5]

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.

<Note>
  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.
</Note>

## 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](/docs/registered-operations), and a caller sends no SQL.
- The `client` export gives applications a remote database proxy, and `client/topology` adds [routing across the nodes](/docs/topology-routing) of a replication group.
- The `react` export turns a [live query](/docs/live-queries) 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](https://github.com/assetcorp/sirannon-db#readme) covers every feature area, including change data capture, migrations, backups, hooks, metrics, and the distributed replication FAQ.
- The [benchmarks](/benchmarks) page documents the suite that compares Sirannon against Postgres 17 and how to reproduce it.
- The [changelog](/changelog) lists what each release added.
