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

@recourse-ai/store-postgres

v0.3.0

Published

Postgres implementation of the recourse Store. Survives more than one instance.

Readme

@recourse-ai/store-postgres

The recourse Store, backed by Postgres.

npm install @recourse-ai/store-postgres pg
import { createChatHandler } from '@recourse-ai/core/server'
import { postgresStore } from '@recourse-ai/store-postgres'

const store = postgresStore({ connectionString: process.env.DATABASE_URL })

createChatHandler({ index, store })

That is the whole integration. Tables are created on first use.

When the other stores stop being enough

memoryStore dies with the process. fileStore assumes one writer.

Every serverless deployment runs more than one instance under load, and with either of those the transcripts, tickets and sources scatter across instances that cannot see each other. Nothing errors; the data is just quietly in the wrong place. That is the failure this package exists to remove.

It also fixes a race the file store cannot: ticket numbers come from a Postgres sequence rather than read-the-highest-and-add-one, so twenty tickets opened in the same moment get twenty different numbers.

Options

postgresStore({
  // Prefer passing a pool you already own: one per process, not one per store.
  pool,
  // Or a connection string, and one is made for you.
  connectionString: process.env.DATABASE_URL,
  // Off if migrations are owned by something else, or the role cannot DDL.
  migrate: true,
})

migrate(pool) is exported if you would rather run it yourself at boot. It takes an advisory lock, so several instances starting at once is fine.

Where it departs from the other stores

  • Message order comes from a sequence, not the timestamp. Two messages in one turn are written in the same millisecond, and ordering on the timestamp renders the answer above the question.
  • Pagination is keyset, not offset. The cursor is still the last item's id, identical to the in-memory store, so a cursor keeps working if you swap implementations. Under it, (updated_at, id) means a row arriving mid-page cannot make you see the same conversation twice.
  • Ticket search is Postgres full-text, over the subject, the description and every message body, through plainto_tsquery so an apostrophe in the search box is a search rather than a 500.
  • stats() is one round trip. Those numbers are read together on every dashboard load, and five queries would be five times the latency.

Vectors

The index file carries int8 vectors and scans them all, which is honest to roughly twenty thousand chunks. Past that it is the file that hurts before the scan does. pgVectorStore moves that half into Postgres:

import { pgVectorStore } from '@recourse-ai/store-postgres'

const vectors = pgVectorStore({ pool, dimensions: 768 })

createChatHandler({ index, embedder, vectorStore: vectors })

Needs CREATE EXTENSION vector. Neon, Supabase and RDS all ship pgvector; a plain Postgres needs it installed.

Chunk text stays in the index file. Only the vectors move, so there is exactly one place the text lives and no rule about which copy wins.

  • An HNSW index with vector_cosine_ops, built on the empty table, so ingest can fill it afterwards. IVFFlat would need a training pass over data that is not there yet.
  • <=> returns cosine distance, so the store returns 1 - distance. Getting that backwards ranks everything upside down and still looks plausible.
  • The relevance floor is applied in SQL, not after, so a million-row table does not ship rows back to be discarded.
  • A dimension mismatch throws rather than returning meaningless distances.

Serverless, where this actually goes wrong

The connection limit, not the query load, is what breaks a database behind serverless functions. Total connections is instances times pool size, and you do not control the first number.

Create the store once, at module scope. The mistake that exhausts a database is building one per request: every call opens another pool, none are closed, and the count climbs until Postgres refuses. The store warns once if it sees a second pool for the same database in one process.

// lib/store.ts, imported by every route, constructed once.
export const store = postgresStore({ connectionString: process.env.DATABASE_URL })

Do not set max: 1. It is common advice and it is wrong: it does not reduce the total, because the total is instances times pool size, and it removes all concurrency inside each instance. Put a pooler in front instead.

Use your provider's pooled endpoint, not the direct one. Neon and Supabase both run PgBouncer in transaction mode and give you a separate host or port for it; a direct connection string is for migrations and psql, not for an application that scales out.

Suspension leaks connections. An idle serverless instance is suspended in memory, and a suspended instance does not run its idle timers, so connections opened before it went to sleep stay open until the instance dies or the database gives up on them. The defaults here use a five second idle timeout to shrink that window. On Vercel, close it properly:

import { attachDatabasePool } from '@vercel/functions'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })
attachDatabasePool(pool)

export const store = postgresStore({ pool })

Defaults when this package builds the pool: max: 10, idleTimeoutMillis: 5000, connectionTimeoutMillis: 10000, and allowExitOnIdle so a script can exit rather than being held open by an idle connection. Override max and idleTimeoutMillis if you have measured something better.

Running the tests

docker run -d -p 55432:5432 -e POSTGRES_PASSWORD=recourse -e POSTGRES_DB=recourse postgres:16-alpine
TEST_DATABASE_URL=postgres://postgres:recourse@localhost:55432/recourse pnpm test

An installed Postgres works too, with no daemon and nothing left behind. The recipe is in the header of test/postgres.test.ts, including the one thing that catches people out: a unix socket path has about a hundred bytes to play with, so the socket directory has to sit somewhere short rather than beside a temporary data directory under a long project path.

Use pnpm test postgres for that route. The vector suite next door needs the pgvector extension, which a plain server does not have, and its failures say extension "vector" is not available rather than anything about your changes.

Without TEST_DATABASE_URL the suite skips rather than fails, so a contributor changing the widget is not blocked by a database they never touched. CI runs it against a service container on every push.

The suite is mostly not written here: it imports the same behaviour assertions the memory and file stores pass. A store that only passes tests written for it is not interchangeable with anything.

MIT