# SQLite



`@postel/sqlite` is a **standalone** adapter: Postel owns the SQLite database. It's the simplest way to get durable, single-node webhook delivery — no server to run.

<Install packages="@postel/sqlite better-sqlite3" />

Hand `SqliteStorage` a `better-sqlite3` database your app already opened — Postel reuses the handle instead of opening its own:

```ts title="lib/postel.ts"
import Database from "better-sqlite3";
import { Postel } from "@postel/core";
import { SqliteStorage } from "@postel/sqlite";

const db = new Database("postel.db");

export const postel = Postel({
  outbound: {
    storage: SqliteStorage({ db }),
  },
});
```

Or give it a filename and Postel opens and owns the database — the simplest path (use `":memory:"` for tests):

```ts title="lib/postel.ts"
import { Postel } from "@postel/core";
import { SqliteStorage } from "@postel/sqlite";

export const postel = Postel({
  outbound: {
    storage: SqliteStorage({ filename: "postel.db" }),
  },
});
```

## Options [#options]

| Option        | Default      | Notes                                                                      |
| ------------- | ------------ | -------------------------------------------------------------------------- |
| `filename`    | `":memory:"` | Path Postel opens. Use a real file for persistence; `:memory:` for tests.  |
| `db`          | —            | An existing `better-sqlite3` `Database` to reuse instead of `filename`.    |
| `autoMigrate` | `true`       | Run migrations on construction. Set `false` to manage migrations yourself. |
| `clock`       | system clock | Inject a clock for deterministic time in tests.                            |

Pass either `filename` (Postel opens it) or `db` (you opened it). `better-sqlite3` is a peer dependency you install alongside.

## Migrations [#migrations]

With `autoMigrate` on (the default), the adapter brings the database up to the current schema version on every construction. It's idempotent — a fully-migrated database is left untouched — so it's safe to run on every boot. The schema it applies is the canonical one documented in [Schema & migrations](/docs/storage/schema).

## Polling, not push [#polling-not-push]

SQLite has no `LISTEN`/`NOTIFY`, so the adapter declares `capabilities.notify = false` and Postel's workers **poll** the outbox at a short interval instead of being pushed. Delivery is identical; only the wake-up latency differs from a Postgres deployment.

## Single-writer [#single-writer]

SQLite serializes writes through a single writer. The adapter reserves outbox rows under `BEGIN IMMEDIATE`, which is correct and safe, but throughput is bounded by one writer — `@postel/sqlite` is built for single-node deployments. For multi-node / high-throughput delivery, use `@postel/pg`.

## Receiver-side dedup [#receiver-side-dedup]

`@postel/sqlite` also exports `SqliteDedup` — the inbound idempotency-dedup helper, independent of the outbound storage. Wire it onto an inbound source, reusing the same database:

```ts
import { SqliteDedup } from "@postel/sqlite";

const postel = Postel({
  inbound: {
    vendor: {
      verify: Secret(config.webhookSecret),
      dedup: SqliteDedup({ db }),
      dedupTtl: "24h",
    },
  },
});
```

Uses `INSERT OR IGNORE` — same contract as the other adapters. Expired rows are purged periodically. See [Deduplication](/docs/inbound/deduplication) for the contract and where to place the call.
