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

@wtfalch/db

v0.3.1

Published

App-owned PostgreSQL connections with shared SQL and native Drizzle transactions

Readme

@wtfalch/db

App-owned PostgreSQL connections with parameterized SQL and optional native Drizzle access. Node.js 22/24 and PostgreSQL 16 are the initial validation targets.

import { createDatabase } from '@wtfalch/db/postgres';

const connection = createDatabase({
  url: () => process.env.DATABASE_URL,
  applicationName: 'my-app',
  max: 5, // choose from your replica/worker connection budget
});
const db = connection.database;
const rows = await db.query<{ id: string }>(
  'select id from items where owner_id = $1',
  [authenticatedUser.id],
);
await db.transaction(async (tx) => {
  await tx.query('insert into items(id) values ($1)', ['example']);
  await tx.transaction(async (savepoint) => {
    await savepoint.query('select 1');
  });
});
await connection.close({ timeoutMs: 5000 });

The app supplies authorization, schema, migrations, credentials and pool budgets. SQL generics assert an expected row shape; they do not validate database results. A query returns rows, including an empty array for commands without RETURNING. Use unique aliases for result columns; duplicate names fail instead of overwriting.

Connection options

url is a PostgreSQL URL or a lazy resolver returning one. It is captured on successful initialization, with no owner-credential fallback. applicationName defaults to wtfalch-db, max to 10, and connectTimeoutMs to 10,000. idleTimeoutMs is optional. Configure each app's budget explicitly.

statementTimeoutMs sets PostgreSQL's own statement_timeout for every connection this owner opens: a shared cutoff so one runaway query cannot hold a connection indefinitely against apps sharing Postgres capacity. It has no default — unset, a statement runs until it finishes, errors, or the connection closes. A cancelled statement fails with SQLSTATE 57014.

query(text, values, options) takes an optional third argument for a single call: signal (an AbortSignal) and/or timeoutMs each cancel that one query in flight — a real PostgreSQL cancel request, not a client-side abandon — on top of any statementTimeoutMs, whichever fires first:

const controller = new AbortController();
setTimeout(() => controller.abort(), 500);
await db.query('select pg_sleep(10)', [], { signal: controller.signal });
// or, equivalently for a single call:
await db.query('select pg_sleep(10)', [], { timeoutMs: 500 });

For authenticated remote TLS, use tls: 'verify-full' and supply ca as PEM text (or an array of PEM certificates) when the provider uses a private CA. Supplying ca with any other tls value, including the default, is a configuration error. require encrypts without verifying the server certificate; prefer permits plaintext fallback. disable is intended for controlled local setups. When tls is omitted, the driver's URL/environment TLS configuration applies. A successful connection alone is not proof of the intended TLS policy.

Use a standard single-host URL, with the database in its path and connection credentials in its authority. Multihost URLs and libpq-style ?host= Unix-socket URLs are not supported. URL options follow Postgres.js semantics, not libpq.

Optional Drizzle

SQL-only installs do not need Drizzle. ORM users install a drizzle-orm version in the supported range, >=0.39.3 <1.0.0 — measured across both ends, not just assumed — shared with packages that inspect native transaction identity. The driver dependency is Postgres.js ^3.4.9. withDrizzle checks the installed drizzle-orm version against this range at first use and throws if it is outside it — a caller-satisfied peer range (^0.39.0 still admits 0.39.0 through 0.39.2, below this package's own tested floor) is not proof the installed version was ever measured against this codec.

import { createDatabase } from '@wtfalch/db/postgres';
import { withDrizzle } from '@wtfalch/db/drizzle';
import * as schema from './schema.js';

const connection = createDatabase({ url: () => process.env.DATABASE_URL });
const db = withDrizzle(connection, { schema }); // before first use
await db.transaction(async (tx) => {
  await tx.orm.insert(schema.items).values({ id: 'example' });
  await tx.query('insert into audit_events(item_id) values ($1)', ['example']);
});

SQL and ORM inside this transaction use the same connection. Nested transactions use savepoints. ORM mappings and errors remain native. Ordinary app functions that accept a context provide named operations; there is no generic binding layer. An arbitrary existing driver pool cannot be adopted.

Handle types for package authors

A package that does not know the app's schema cannot name DrizzleDatabase<TSchema> or DrizzleTransaction<TSchema> directly. Accept AnyDrizzleDatabase and AnyDrizzleTransaction from @wtfalch/db/drizzle instead — the same shapes with the schema erased to Record<string, unknown> — and read the query builder, .execute(sql) and .transaction() surface through .orm, exactly as with the parameterised types.

A package that instead holds only the native Drizzle handle — tx.orm itself, not the SDK's wrapped view — faces the same erasure problem. NativeDatabase<TSchema> and NativeTransaction<TSchema> name that native handle's parameterised type; AnyNativeDatabase and AnyNativeTransaction are their schema-erased forms, so a package taking only tx.orm does not have to hand-roll PgDatabase<PgQueryResultHKT, any, any> itself:

import type { AnyNativeTransaction } from '@wtfalch/db/drizzle';

async function audit(native: AnyNativeTransaction) {
  await native.insert(auditEvents).values({ event: 'example' });
}

tx.orm is Drizzle's own native transaction object, not a lookalike, for every drizzle-orm version this package's peer range admits. But instanceof tests class identity by the exact PgTransaction module a package's own import resolved, and that check fails whenever a package's drizzle-orm and this package's drizzle-orm land as two separate installed copies in the same dependency tree — a real, if quiet, monorepo hazard, and not something this package's contract can promise its way around. Use Drizzle's own is() helper instead of bare instanceof: it checks a shared Symbol.for('drizzle:entityKind') tag rather than class identity, so it tolerates multiple installed copies of the same version. packages/db/test/integration.test.ts asserts both directions: instanceof holding in a single-copy install, and failing across two independently installed copies of the same version.

The way back: queryFor

A package that receives only a native transaction handle — tx.orm, typed or erased — has no way back to SDK SQL on that same connection:

import { queryFor } from '@wtfalch/db/drizzle';

await db.transaction(async (tx) => {
  await audit.record(tx.orm); // a package holds only the native handle
  await queryFor(tx.orm)('insert into audit_events(item_id) values ($1)', [
    'example',
  ]);
});

queryFor(handle) returns the same query function tx.query exposes, bound to that transaction's own connection. A nested transaction's native handle resolves to its own savepoint's connection, never the outer one. A handle from a different transaction, or one whose transaction already committed or rolled back, throws — it never falls back to the pool, where a raw statement would commit or fail independently of the transaction it appears to run inside. See packages/db/test/integration.test.ts for the same-connection commit, same-connection rollback, savepoint-rollback and both-throws proofs against real PostgreSQL.

Nesting registers with queryFor the same way whether it goes through the SDK's own tx.transaction(...) or through the native handle directly:

await db.transaction(async (tx) => {
  await tx.orm.transaction(async (nested) => {
    // nested never went through tx.transaction(...); queryFor still finds it.
    await queryFor(nested)('insert into audit_events(item_id) values ($1)', [
      'example',
    ]);
  });
});

A package that only received tx.orm — never the SDK tx — and nests with it needs no bridge of its own to keep queryFor working on what it opens.

Construction and schema attachment do not resolve the URL or open connections. Attach once during setup; repeated attachment with the same schema object returns the same view, while a different schema or attachment after first use is rejected. Packages borrow execution contexts, which have no top-level close method. The app retains the owner. This is lifecycle separation, not a security sandbox over native ORM objects.

SQL values and errors

SQL results decode int2/int4/oid and floats as numbers, int8/numeric as exact strings, booleans as booleans, JSON as JS values and bytea as Buffer. Finite timestamptz becomes Date with millisecond precision; infinity remains text. Date/time/timestamp without timezone and unknown types remain text. Supported built-in arrays preserve nesting and null elements. For exact timestamp cursors, select text; JSON numbers have ordinary JS precision limits. Native ORM mappings may differ, particularly arrays and column timestamp modes.

Bind values separately from SQL. bigint is exact decimal text, valid Date is ISO text, Buffer stays binary and plain objects are JSON encoded. JS arrays mean SQL arrays; explicitly JSON-encode an array intended as JSON. Undefined, invalid Dates and unsafe integer numbers fail. Extension/composite/domain types and nonstandard array lower bounds require explicit casts to supported types.

import { sqlState } from '@wtfalch/db';
if (sqlState(error) === '23505') {
  // Handle a domain-specific unique conflict.
}

The classifier follows cause chains and handles cycles. Underlying driver and ORM errors stay intact, which means they may contain sensitive SQL, parameters or server details: never log whole error objects. The SDK does not retry callbacks or log credentials. Configure application error reporting at the trusted boundary.

Shutdown

Await every statement and nested transaction within its callback. SDK transaction contexts reject later use after completion. close is terminal and idempotent; the first call sets its deadline and repeated calls share its promise. New SDK root work is refused while accepted managed work drains. The deadline terminates the pool and rejects close if work has not finished. Timeouts must be between 0 and 2,147,483,647 milliseconds.

Stop producers and await direct native ORM queries before closing. Native query builders/handles can escape tracking; root native ORM transactions do not receive all SDK lifetime guarantees. Forced shutdown cannot cancel arbitrary JavaScript or establish whether a racing commit succeeded. Use domain reconciliation for an ambiguous result instead of blindly replaying writes.

Health checks

connection.ping() runs a trivial statement and resolves on success, or rejects on failure or once timeoutMs (default 5000) elapses first — a readiness probe for a deploy target such as Coolify, without hand-writing db.query('select 1'):

app.get('/healthz', async (_req, res) => {
  try {
    await connection.ping({ timeoutMs: 2_000 });
    res.sendStatus(200);
  } catch {
    res.sendStatus(503);
  }
});

adaptPostgres owners expose the same ping(), over the caller's own client. A timed-out ping does not cancel the underlying query; it only stops waiting for it.

Migrations

@wtfalch/db/migrate applies pending *.sql files from a directory, one transaction per file, recorded in an _migrations(filename, applied_at) ledger so a re-run applies nothing:

import { runMigrations } from '@wtfalch/db/migrate';

const { applied } = await runMigrations({
  url: process.env.DATABASE_URL,
  migrationsDir: 'drizzle',
});

Files run in filename order (name them so lexical order is application order) over a session-scoped advisory lock (default id 728143) held for the whole run, so two callers migrating the same database concurrently serialize instead of racing. A failure rolls back only that file; earlier files stay committed. Each migration's SQL runs unprepared, over one connection this call opens and closes itself — multiple statements and dollar-quoted DO $$ ... $$ blocks in one file are fine, but no psql meta-command (\i, \gset, ...) is available, since nothing here shells out to psql.

The package also ships a CLI, db-migrate, reading the same DATABASE_URL and MIGRATIONS_DIR (default /app/drizzle) environment variables app-template's scripts/migrate.sh does, waiting up to 60 seconds for the database to become reachable before applying — a drop-in for that script with no psql/libpq install required:

DATABASE_URL=postgres://... MIGRATIONS_DIR=drizzle npx db-migrate

Test harness

@wtfalch/db/testing starts a disposable PostgreSQL in Docker for an integration suite — the same harness this package's own suite runs in. It needs docker on PATH, binds to a random 127.0.0.1 port and is for tests only (the password is a fixed constant):

import { after, before } from 'node:test';
import { startEphemeralPostgres } from '@wtfalch/db/testing';

let pg;
before(async () => {
  pg = await startEphemeralPostgres(); // { image?, database?, readyTimeoutMs? }
  process.env.DATABASE_URL = pg.url;
});
after(() => pg?.stop());

stop() removes the container and is safe to call twice.

See the included provisioning guide for roles, migration ownership, backup/restore checks and consumer adoption requirements. Provisioning, package publication and production cutovers are separate operations.