SQLite
@postel/sqlite — a standalone SQLite storage adapter built on better-sqlite3.
@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.
pnpm add @postel/sqlite better-sqlite3npm install @postel/sqlite better-sqlite3yarn add @postel/sqlite better-sqlite3bun add @postel/sqlite better-sqlite3Hand SqliteStorage a better-sqlite3 database your app already opened — Postel reuses the handle instead of opening its own:
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):
import { Postel } from "@postel/core";
import { SqliteStorage } from "@postel/sqlite";
export const postel = Postel({
outbound: {
storage: SqliteStorage({ filename: "postel.db" }),
},
});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
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.
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
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
@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:
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 for the contract and where to place the call.