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

@howells/neon

v0.2.0

Published

Hardened Neon Postgres client layer: resilient fetch, narrow transport retries, opt-in IPv4-first, HMR-safe pools, endpoint guards.

Readme

@howells/neon

Hardened Neon Postgres client layer. One package encoding every fix the portfolio learned the hard way: resilient fetch, narrow transport retries, opt-in IPv4-first DNS, HMR-safe pools, and pooled/direct endpoint guards.

pnpm add @howells/neon

Drivers are optional peer dependencies — install only what the subpath you use needs: @neondatabase/serverless for /http, pg for /pool and /mastra, drizzle-orm for /http and /pool (not /mastra), drizzle-kit for /kit.

Why

A survey of 19 Neon-backed repos found the same six failure classes fixed ad hoc, over and over:

  1. IPv6 happy-eyeballs stall — Neon publishes AAAA records some networks can't route to AWS; Node races a dead IPv6 connection and stalls ~14s.
  2. Autosuspend cold starts vs. tight timeouts — Neon suspends idle computes and terminates their connections; Postgres' 5s default connect timeout can't survive the wake.
  3. HMR pool leaksnext dev re-evaluates db modules; module-level singletons leak a fresh pool per reload until the connection limit dies.
  4. Idle-drop errors killing the process — a dropped idle pg client emits 'error'; unhandled, it takes the process down.
  5. Transient fetch failed — the HTTP driver is one fetch per query with no retry.
  6. Endpoint misuse — apps stacking pools on the direct endpoint exhaust its compute-sized connection limit; migrations through PgBouncer hit transaction-mode gotchas.

This package is those fixes, versioned.

Quickstart

App data access (default) — @howells/neon/http

import { createHttpDb } from "@howells/neon/http";
import * as schema from "./schema";

export const db = createHttpDb({ url: databaseUrl, schema });
export type Db = typeof db;

Stateless HTTP driver with a narrow retrying fetch installed and a fresh Drizzle wrapper on every call, so HMR picks up schema changes. Pass the pooled (-pooler) URL — the factory asserts it.

Interactive transactions — @howells/neon/pool

import { createPooledDb } from "@howells/neon/pool";
import * as schema from "./schema";

export const db = createPooledDb({ url: databaseUrl, schema });

Hardened pg pool: max 5, 15s connect timeout (cold starts), 20s idle timeout (release before Neon drops server-side), keepalive, an error listener so idle drops can't kill the process, and globalThis caching so HMR reuses one pool. On Vercel Fluid, pass onPoolCreated: attachDatabasePool from @vercel/functions — unless the code relies on SET LOCAL continuity, which it breaks.

Persistent sessions and LISTEN/NOTIFY

Neon's PgBouncer uses transaction pooling and does not support persistent LISTEN sessions (restrictions). Use a direct URL and hold a checked-out client for the listener's lifetime:

import { createNeonPool } from "@howells/neon/pool";

const listenerPool = createNeonPool({ url: directDatabaseUrl, max: 1 });
const listener = await listenerPool.connect();
listener.on("error", handleListenerError); // Application owns reconnect/resubscribe.
listener.on("notification", handleNotification);
try {
  await listener.query("LISTEN updates");
  await untilShutdown();
} finally {
  listener.release(true); // Destroy the session, including its subscriptions.
  await listenerPool.end();
}

For Drizzle sessions, createPooledDb({ url: directDatabaseUrl, allowDirect: true }) makes the endpoint exception explicit; session state still requires a checked-out client. Ordinary interactive transactions use the pooled URL and db.transaction(...).

Migrations — @howells/neon/kit

// drizzle.config.ts
import { neonKitConfig } from "@howells/neon/kit";

export default neonKitConfig({
  directUrl: process.env.DIRECT_DATABASE_URL ?? "",
  schema: "./packages/db/src/schema.ts",
});

Asserts the direct endpoint — never push schema through PgBouncer.

Mastra storage — @howells/neon/mastra

import { createMastraPool, mastraPoolOptions } from "@howells/neon/mastra";

const storage = new PostgresStore({
  id: "app-storage",
  pool: createMastraPool({ url }),
});
const vectors = new PgVector({
  connectionString: url,
  pgPoolOptions: mastraPoolOptions({ url }),
});

Pass a URL without SSL or host query parameters to PgVector and mastraPoolOptions; the helper rejects them because pg URL parsing can override its TLS options (pg SSL precedence). Use the same URL for both arguments.

createMastraPool reuses a pool by name/url, forwards onIdleError and onPoolCreated, and joins the closeAll() registry. The first call owns tuning and callbacks. The application owns shared shutdown; do not call pool.end() from a hot-reloaded module or independently close a shared pool. PgVector owns its separate pool and must be shut down through its own lifecycle.

One config, two pools: PgVector accepts no injected pool, so both derive from the same resolver and cannot drift. max is clamped ≥ 2 — a single-client pool deadlocks @mastra/pg batch writes. @mastra/pg silently drops connectionTimeoutMillis from pgPoolOptions-style config, which is why createMastraPool builds the pool itself.

Lint enforcement — @howells/neon/lint

import { createOxlintConfig } from "@howells/neon/lint";

Bans drizzle-orm/neon-serverless and Pool/Client from @neondatabase/serverless via no-restricted-imports. Stopgap until the @howells/lint fleet preset carries the same rules.

createOxlintConfig({ restrictPgOutsideDb: true }) additionally bans bare pg imports everywhere except the packages that own a direct Postgres connection (pgAllowedIn, default packages/db/** and packages/mastra/**). The carve-out is a positive-glob override rather than negated globs because oxlint ignores !-negation in override files arrays. Overrides replace the top-level rule config for matching files, so if your own oxlint config declares no-restricted-imports in an override that also matches those packages, merge the fragment's paths rather than clobbering them.

Retry safety — read this before touching the knobs

The default retry matcher (isConnectionError) is narrow: it only retries failures where the request provably never reached the server (refused, unresolved DNS, connect-phase timeout). This applies to a single transport attempt, not an arbitrary callback that may already have completed other writes.

The broad matcher (isTransientNeonError) also retries mid-flight drops (fetch failed, ECONNRESET) — where the server may already have executed the statement. Retrying a non-idempotent write through it can double-apply. Use it only via retryDbRead, or pass it to createResilientFetch only when the workload is read-only or idempotent.

Neither matcher ever retries a real query failure: a non-connection SQLSTATE (constraint violation, syntax error) is authoritative and refuses retry regardless of what the message contains.

withNeonRetry(op) runs once by default. Retrying the whole callback requires withNeonRetry(op, { idempotent: true }); this is your assertion that every operation in it can safely run again. retryDbRead makes that assertion for read-only callbacks.

installResilientFetch is first-writer-wins per actual driver configuration. ESM and CJS driver instances are installed independently. Call it at startup to tune timing; custom global retry classifiers are rejected. createHttpDb and createResilientSql use this global narrow policy and accept no per-client fetch configuration.

Driver decision table

| Situation | Use | | --- | --- | | Next.js app code, serverless, Workers, scripts | /http | | Interactive transactions, long-running services, batch work | /pool + pooled URL | | Persistent LISTEN/NOTIFY sessions | /pool + direct URL + checked-out client | | Schema push / migrations | /kit + direct URL | | Mastra PostgresStore / PgVector | /mastra | | The Neon WebSocket driver (neon-serverless) | Don't. Idle-socket drops and HMR leaks; the one documented exception is edge runtimes that genuinely need the WS pool (neonConfig.poolQueryViaFetch). |

Caching

TCP pools cache on globalThis under a versioned Symbol.for key, which survives both next dev HMR and the ESM/CJS dual-package hazard. Entries are never evicted — keys must be low-cardinality. Never derive name or url from per-tenant/dynamic values, or each distinct key pins a pool for the life of the process. Call closeAll() in test teardown so cached pools don't hang vitest.

HTTP wrappers are never cached. /pool and /mastra cache connection pools in separate namespaces; schema wrappers are always fresh. closeAll() closes and clears registered pools at application shutdown or test teardown. Raw createNeonPool callers own pool.end() themselves.

Network policy

Factories do not change process-wide DNS ordering or automatic address-family selection. On a Node host with broken IPv6 routing, explicitly call preferIpv4() from @howells/neon at application startup. This changes both settings for the entire process.

0.2.0 release: migrating from 0.1.x

This release tightens retry safety, makes network policy explicit, and unifies pool ownership across ESM and CJS consumers. It changes retry and configuration APIs; review these steps before upgrading.

  • Add idempotent: true only to callbacks that are safe to repeat; otherwise withNeonRetry now runs once.
  • Move HTTP timing options to installResilientFetch at startup. Remove per-client fetchOptions and the second argument to createResilientSql; broad global retry policies are rejected.
  • HTTP name remains accepted for compatibility but is ignored. Do not rely on wrapper identity.
  • Mastra pools now share ownership by name/URL; close them with closeAll() at application shutdown. Remove SSL/host query parameters from the URL used with mastraPoolOptions and PgVector.
  • Call preferIpv4() explicitly if your application needs the previous network policy.
  • Generic cache helpers getOrCreate and markOnce are internal; use the pool factories and closeAll() instead.

Environment contract

This package never reads process.env. Bring your own URLs — pair with @howells/envy:

  • DATABASE_URL — pooled (-pooler) host, for /http, /pool, /mastra
  • DIRECT_DATABASE_URL — direct host, for /kit and admin tooling

Recommended connection-string params (or use withNeonParams): sslmode=verify-full&channel_binding=require&connect_timeout=15.

Per-environment branch selection (a dev Neon branch's URL in local .env, production's in Vercel) is env-layer work, not client-layer — do it in your envy schema, then hand the resolved URL here.

Compatibility

| drizzle-orm | Factories (/http, /pool) | Primitives (.) | | --- | --- | --- | | ^0.45 | ✅ | ✅ | | 1.0.0-rc (relational v2) | ❌ use primitives + your own drizzle({ client, relations }) | ✅ |

The 0.45 peer range is a stopgap: when drizzle 1.x GAs and the fleet moves, the factories grow a real 1.x branch.

Non-goals

No process.env reads. No schema or migration opinions beyond /kit's direct-URL enforcement. No query builders. No WebSocket-driver support. No logging framework — injectable callbacks only.