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 kyselynpm install @postel/kysely kyselyyarn add @postel/kysely kyselybun add @postel/kysely kyselyPlus your database driver — pg, mysql2, or better-sqlite3.
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
| Option | Notes |
|---|---|
db | Your 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. |
autoMigrate | Run migrations on first use (default true). |
clock | Inject 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.