Storage

Kysely

@postel/kysely — run Postel's storage through your existing Kysely query builder (Postgres, MySQL, or SQLite).

@postel/kysely is an ORM/query-builder adapter: you hand Postel the Kysely instance you already use, and it issues its queries (and transactions) through it. That means an outbox insert shares your connection and composes with your own writes — no second pool.

pnpm add @postel/kysely kysely
npm install @postel/kysely kysely
yarn add @postel/kysely kysely
bun add @postel/kysely kysely

Plus your database driver — pg, mysql2, or better-sqlite3.

lib/postel.ts
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
import { Postel } from "@postel/core";
import { KyselyStorage } from "@postel/kysely";
import { config } from "./config.js";

const db = new Kysely<DB>({
  dialect: new PostgresDialect({
    pool: new Pool({ connectionString: config.databaseUrl }),
  }),
});

export const postel = Postel({
  outbound: {
    storage: KyselyStorage({ db, dialect: "postgres" }),
  },
});

Options

OptionNotes
dbYour Kysely instance.
dialect"postgres", "mysql", or "sqlite" — selects the reservation strategy (FOR UPDATE SKIP LOCKED on Postgres/MySQL, ordered on SQLite), the capability flags (notify on for Postgres), and the column codecs.
autoMigrateRun migrations on first use (default true).
clockInject a clock for deterministic time in tests.

kysely is a peer dependency. Postel runs through Kysely's sql tag, so your Kysely<DB> needs no Postel-specific table typing. The dialect names a SQL family, so wire-compatible engines (MariaDB, PlanetScale, libSQL/Turso, …) work via the matching dialect — see compatible databases.

On MySQL, build your Kysely instance with a callback-style mysql2 pool — import { createPool } from "mysql2", as Kysely's MysqlDialect documents. A mysql2/promise pool leaves Kysely's getConnection/end callbacks unfired, so every query hangs; this is a Kysely requirement, not Postel-specific.

Shared transactions

Open a Kysely transaction and pass it to send() — the outbox insert commits atomically with your business writes:

await db.transaction().execute(async (trx) => {
  await trx.insertInto("orders").values({ /* ... */ }).execute();
  await postel.send({ type: "order.created", data: { /* ... */ } }, { tx: trx });
});

Migrations

autoMigrate (default true) runs the canonical migrations through your connection, version-gated and idempotent — or run your own tooling against that schema.

On this page