Storage

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-sqlite3
npm install @postel/sqlite better-sqlite3
yarn add @postel/sqlite better-sqlite3
bun add @postel/sqlite better-sqlite3

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

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

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

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

Options

OptionDefaultNotes
filename":memory:"Path Postel opens. Use a real file for persistence; :memory: for tests.
dbAn existing better-sqlite3 Database to reuse instead of filename.
autoMigratetrueRun migrations on construction. Set false to manage migrations yourself.
clocksystem clockInject 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.

On this page