Client adapters

Kysely

Run Kysely queries against Minnow in the main thread or a worker.

@minnowdb/kysely lets an existing Kysely application use Minnow as its browser-local database. It uses Kysely's PostgreSQL compiler, so generated statements use double-quoted identifiers, $1 parameters, RETURNING, and the PostgreSQL forms Minnow supports.

npm install @minnowdb/core @minnowdb/kysely kysely

The adapter supports Kysely 0.29.x.

Define once and connect

Pass the same schema you migrate to createKysely. It derives Kysely's complete DB map, so tables and columns are not retyped. migrate() changes the database; createKysely() only creates the query interface, so keep the explicit migration call:

import { createKysely } from "@minnowdb/kysely";
import { MinnowDatabase, column, schema, table } from "@minnowdb/core";
import { IndexedDbBlockStore } from "@minnowdb/core/storage";

const orders = table("orders", {
  order_id: column.integer().unique().autoIncrement(),
  customer: column.string(),
  total: column.numeric({ precision: 12, scale: 2 }).default("0"),
  status: column.enum(["open", "closed"]).default("open"),
  note: column.string().nullable(),
});
const appSchema = schema([orders]);

const database = new MinnowDatabase(await IndexedDbBlockStore.open({ name: "shop" }));
await database.migrate(appSchema);
const db = createKysely({ driver: database, schema: appSchema });

const orderRows = await db
  .selectFrom("orders")
  .select(["order_id", "total"])
  .where("total", ">=", 25)
  .orderBy("total", "desc")
  .execute();

The inferred Kysely columns retain Minnow's separate select, insert, and update types:

  • defaults and nullable columns are optional on insert;
  • enum values remain literal unions;
  • exact NUMERIC selects as a lossless string, while writes and predicates naturally accept string | number;
  • scalar and composite primary-key columns cannot be updated;
  • declared views select normally and accept no column values on insert or update.

If an existing factory constructs Kysely, use the exported type directly:

import { Kysely } from "kysely";
import { MinnowDialect, type InferKyselyDatabase } from "@minnowdb/kysely";

type DB = InferKyselyDatabase<typeof appSchema>;
const db = new Kysely<DB>({
  dialect: new MinnowDialect({ driver: database, schema: appSchema }),
});

Pass schema to the dialect when you construct Kysely yourself. It supplies the exact DB type and lets the compiler normalize a batch of empty value objects. Literal and SQL-expression defaults live only in Minnow's catalog, so Kysely, raw SQL, batches, workers, and other tabs all get the same behavior. There is no adapter-side generation.

An empty Kysely value object compiles to PostgreSQL's normal DEFAULT VALUES form. A batch of empty objects uses DEFAULT value slots, so catalog defaults run once per row without adapter-specific SQL.

The generated SQL stays visible and ordinary. Omitted object properties compile to SQL omission or DEFAULT; explicit null remains a bound NULL:

const query = db.insertInto("orders").values({ customer: "Ada" });
const compiled = query.compile();
// compiled.sql:
// insert into "orders" ("customer") values ($1)
// compiled.parameters: ["Ada"]

The catalog fills order_id, total, and status when the statement executes; Kysely does not hide generated parameters in the compiled query. INSERT ... SELECT may omit default-bearing target columns for the same reason: the engine evaluates their stored SQL once per selected row.

MinnowDatabaseClient works in the same position, so moving the engine to a worker changes only how database is constructed. Both implement MinnowSqlDriver structurally.

Supported Kysely operations

  • Reads, inserts, updates, deletes, and their $n parameters.
  • RETURNING rows and Kysely's affected-row counts.
  • db.transaction().execute(...) through Minnow's BEGIN, COMMIT, and ROLLBACK.
  • Raw SQL savepoints inside that transaction.
  • Kysely schema statements that stay inside Minnow's supported DDL profile.
  • db.introspection: tables, views, columns, nullability, defaults, auto-increment, and logical types. Exact NUMERIC(p, s), JSON/JSONB, UUID, TIME, INTERVAL, arrays, and enum names are reported instead of being flattened to text.

Minnow has one logical connection. Kysely serializes work through it and a transaction sees its own staged writes. db.destroy() releases Kysely but does not close the caller-owned Minnow database or worker client; close that driver separately when the application is done.

Limits

Minnow uses PostgreSQL-style SQL, but it is an embedded database rather than a PostgreSQL server:

  • getSchemas() returns an empty list. There are no catalogs or schemas to invent.
  • Transaction access modes and isolation levels are rejected; Minnow has one snapshot model.
  • The engine accepts SAVEPOINT, ROLLBACK TO, and RELEASE; Kysely's dialect interface exposes one top-level transaction rather than a nested transaction helper.
  • Streaming is rejected because Minnow returns a completed result set.
  • Parameters must be boolean, number, string, Date, or null; BigInt, byte arrays, and driver-native PostgreSQL objects have no Minnow parameter representation. Exact decimals accept numbers or strings as inputs and return lossless strings. JSON/JSONB, UUID, arrays, TIME, intervals, and enums cross this adapter as strings.
  • Kysely can generate PostgreSQL features outside Minnow's profile. Check the executable feature matrix; the engine rejects unsupported SQL explicitly.

Kysely reserves the connection during a migration, so one Kysely instance cannot race itself. That does not coordinate migrations across tabs. For shared application startup, use Minnow's schema declaration and database.migrate(schema), which coordinates through Minnow's catalog.

Compatibility checks

Adapter tests compile and execute DDL, inserts, queries, RETURNING, commits, rollbacks, and introspection. Minnow's broader SQL compatibility suite compares supported PostgreSQL reads and writes with PGlite and checks every documented difference and exclusion.

On this page