npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

kysely-durable-objects

v0.2.2

Published

Kysely dialect for Cloudflare Durable Object SQLite storage (ctx.storage.sql)

Readme

kysely-durable-objects

CI npm

Kysely dialect for Cloudflare Durable Object SQLite storage (ctx.storage.sql). A fully tested, battle-ready dialect that respects the Durable Object runtime's constraints.

Highlights

  • Comprehensively tested in real workerd. 35 tests run inside the same C++ runtime Cloudflare deploys, against real ctx.storage.sql. CI exercises the suite across a matrix of compatibility_date values (DO-SQLite GA → today) so silent API drift in the runtime can't sneak in. A production-parity guard re-runs the full CRUD path with new Function() patched to throw, proving the dialect doesn't rely on dynamic code generation.
  • insertId and numAffectedRows work end-to-end. The dialect queries last_insert_rowid() and changes() after each mutation, so Kysely's RETURNING, numAffectedRows, and MikroORM identity tracking all behave correctly.
  • BigInt parameter binding. DO's storage layer rejects bigint at the binding boundary. The dialect transparently stringifies bigints so SQLite parses them as native 64-bit INTEGER without truncation.
  • Honors DO atomicity semantics. db.transaction() throws an actionable error pointing to the bundled withDoTransaction helper, which wraps ctx.storage.transactionSync(). The dialect never silently swallows a BEGIN — that would turn rollback into a corruption hazard.
  • MikroORM compatible. clone() returns the same instance so MikroORM's Utils.copy() deep-clone of driverOptions preserves the closure around ctx.storage.sql.
  • Real error paths. UNIQUE/NOT NULL violations and SQL syntax errors surface as real exceptions, tested in real workerd.
  • UPSERT (ON CONFLICT DO UPDATE) with RETURNING works end-to-end.

Install

npm install kysely-durable-objects kysely

kysely is a peer dependency. Releases are signed and ship with npm provenance attestations — every published version links back to the exact GitHub Actions run that built it.

A runnable example lives at examples/basic-worker/ — a Cloudflare Worker with a Durable Object using the dialect, including a withDoTransaction block. git clone && cd examples/basic-worker && pnpm install && pnpm dev.

Usage

Kysely

import { Kysely } from 'kysely';
import { DurableObjectSqliteDialect } from 'kysely-durable-objects';
import { DurableObject } from 'cloudflare:workers';

interface Schema {
  users: { id: number; name: string; email: string };
}

export class MyDO extends DurableObject {
  private db: Kysely<Schema>;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.db = new Kysely<Schema>({
      dialect: new DurableObjectSqliteDialect(ctx.storage.sql),
    });
  }
}

MikroORM

MikroORM v7 uses Kysely under the hood and accepts any Kysely dialect via driverOptions.

import { MikroORM } from '@mikro-orm/core';
import { SqliteDriver } from '@mikro-orm/sql';
import { DurableObjectSqliteDialect } from 'kysely-durable-objects';
import compiledFunctions from './compiled-functions.js';

ctx.blockConcurrencyWhile(async () => {
  this.orm = await MikroORM.init({
    driver: SqliteDriver,
    dbName: 'do',
    driverOptions: new DurableObjectSqliteDialect(ctx.storage.sql),
    entities: [...],
    implicitTransactions: false, // required: BEGIN is blocked in DOs
    compiledFunctions,           // required: new Function() is blocked in DOs
  });
});

Pre-generate compiled functions with npx mikro-orm compile. See the MikroORM deployment docs.

Atomicity (withDoTransaction)

db.transaction() throws on this dialect — there is no safe way to bridge Kysely's async stepwise BEGIN/COMMIT/ROLLBACK lifecycle to DO's synchronous transactionSync(closure) primitive. For atomic blocks, use the bundled helper:

import { withDoTransaction } from 'kysely-durable-objects';

withDoTransaction(ctx.storage, (sql) => {
  sql.exec('update accounts set balance = balance - ? where id = ?', 100, fromId);
  sql.exec('update accounts set balance = balance + ? where id = ?', 100, toId);
});

Throwing inside the closure rolls back. The closure is synchronous — that's a hard constraint of transactionSync.

Tested in real workerd

35 tests, all running inside the same C++ runtime Cloudflare deploys (via @cloudflare/vitest-pool-workers), against real ctx.storage.sql:

  • CRUD & isolation — schema builder, INSERT/SELECT/UPDATE/DELETE, RETURNING, auto-increment, per-DO storage isolation, runInDurableObject introspection, destroy() semantics.
  • Eval-guarded CRUD — full CRUD path re-run with Function/eval patched to throw, simulating the production new Function() ban that the local runner relaxes.
  • Error paths — UNIQUE/NOT NULL constraint violations and SQL syntax errors surface as real exceptions.
  • UPSERTINSERT ... ON CONFLICT DO UPDATE with RETURNING.
  • withDoTransaction — commit and rollback semantics under real transactionSync.
  • Kysely Migrator end-to-end — runs real migrations, verifies the kysely_migration tracking table populates correctly, and that the target schema lands.
  • Type fidelityNULL across all column affinities, BLOB (Uint8Array) bit-exact roundtrip, BigInt within and beyond JS safe-integer range, REAL precision, dates as integer ms, JSON text + json_extract().
  • Concurrency — reproduces the await-boundary lost-update race and proves blockConcurrencyWhile mitigates it.

CI runs the full suite across a matrix of compatibility_date values, so any silent change to DO storage or Workers runtime behavior between releases is caught.

npm test

Production smoke tests

A separate workflow (.github/workflows/smoke.yml) deploys a focused smoke worker to a real Cloudflare account, runs a subset of the suite over HTTP against the actual production runtime, and tears the deployment down. Runs weekly on a cron and can be triggered manually. Requires three repo secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, SMOKE_TOKEN (random gating value).

This catches divergences between local workerd and the production runtime that the local matrix can't see.

API

new DurableObjectSqliteDialect(sql: SqlStorage): Dialect

withDoTransaction<T>(
  storage: { sql: SqlStorage; transactionSync<T>(fn: () => T): T },
  closure: (sql: SqlStorage) => T,
): T

introspectSchema(sql: SqlStorage): TableSchema[]
generateKyselyDbInterface(tables: TableSchema[], opts?: { interfaceName?: string }): string

Type re-exports for convenience: SqlStorage, SqlStorageCursor, DurableObjectStorageLike, ColumnInfo, TableSchema — mirror @cloudflare/workers-types so this package has no hard dependency on it.

Schema introspection (migration aid)

When migrating a hand-rolled ctx.storage.sql.exec() codebase into Kysely, run introspectSchema(ctx.storage.sql) from inside the DO and pipe the result through generateKyselyDbInterface() to bootstrap a Kysely DB interface from your existing schema. The result is a string of TypeScript you can paste into your project. SQLite type affinities map to TypeScript as INTEGER/REALnumber, TEXTstring, BLOBUint8Array, anything else → unknown. Nullable columns become T | null.

Migrations

Kysely's built-in Migrator works against this dialect end-to-end. The kysely_migration and kysely_migration_lock tables are created automatically on first run; the lock table is semantically redundant inside a DO (per-instance serialization already prevents concurrent migration), but harmless.

Compatibility

| | Tested against | |---|---| | Kysely | ^0.28.15 (peer dep >=0.27.0) | | MikroORM | v7+ (uses Kysely under the hood) | | Cloudflare compatibility_date | 2024-09-23 (DO SQLite GA) → 2026-04-06 (current); CI exercises 4 dates across that range | | Node | 22 for the test/build toolchain (workerd ships its own runtime) | | Package manager | [email protected] (pinned in packageManager) |

Kysely is a peer dependency. MikroORM is supported via driverOptions; see Usage → MikroORM.

Platform limitations

Properties of the Durable Object runtime that consumers need to plan around. The dialect surfaces these honestly rather than hiding them.

  • new Function() / eval() are banned during request handling. The dialect's hot path is eval-free; if you use MikroORM, configure compiledFunctions to ship pre-compiled query plans.
  • No raw BEGIN/COMMIT/ROLLBACK/SAVEPOINT. DO storage exposes atomicity only via ctx.storage.transactionSync(closure). Use withDoTransaction (above).
  • INTEGER columns return as JS Number. BigInt values up to 2^53 roundtrip bit-exactly. Beyond that, the value lands in storage intact (the dialect coerces on write) but a direct read rounds. Recover the full 64-bit value with CAST(col AS TEXT):
    SELECT CAST(big_id AS TEXT) AS big_id_str FROM ledger WHERE ...
  • changes() and last_insert_rowid() aren't returned by exec(). The dialect issues two extra SELECT calls after each mutation to retrieve them. Safe — storage operations inside a DO are serialized.
  • Async workflows can interleave at await boundaries. Per-instance serialization is at the storage-operation level, not at the application code level. For read-modify-write across awaits, wrap with ctx.blockConcurrencyWhile.

Performance

Single-row inserts, measured against local workerd 2026-04-30 on a developer machine (2000 ops, indicative only — CI runners and production hardware will differ):

| Path | Throughput | Time / 2000 ops | |---|---|---| | Raw ctx.storage.sql.exec() | ~500K ops/s | ~4ms | | Kysely via this dialect | ~44K ops/s | ~45ms |

The ~11× cost on the Kysely path is dominated by Kysely's query compilation plus the two SELECTs the dialect issues after each mutation to retrieve changes() and last_insert_rowid(). The benchmark is part of the workerd suite and emits numbers via console.log on every CI run, so regressions show up in PR logs.

Releases

See GitHub Releases for the changelog.

License

MIT