PostgreSQL storage adapter for Keyv
PostgreSQL storage adapter for Keyv.
Requires Postgres 9.5 or newer for ON CONFLICT support to allow performant upserts. Why?
- Install
- Usage
- Migrating to v6
- Constructor Options
- Properties
- Methods
- Using an Unlogged Table for Performance
- Connection Pooling
- SSL/TLS Connections
- Testing
- License
npm install --save keyv @keyv/postgresimport Keyv from 'keyv';
import KeyvPostgres from '@keyv/postgres';
const keyv = new Keyv({ store: new KeyvPostgres('postgresql://user:pass@localhost:5432/dbname') });
keyv.on('error', handleConnectionError);You can specify the table and schema options:
const keyvPostgres = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', table: 'cache', schema: 'keyv' });
const keyv = new Keyv({ store: keyvPostgres });You can also use the createKeyv helper function to create Keyv with KeyvPostgres store:
import { createKeyv } from '@keyv/postgres';
const keyv = createKeyv({ uri: 'postgresql://user:pass@localhost:5432/dbname', table: 'cache', schema: 'keyv' });The opts getter still exists for backward compatibility but should not be used for new code.
In v5, namespaces were stored as key prefixes in the key column (e.g. key="myns:mykey" with namespace=NULL). In v6, the namespace is stored in a dedicated namespace column (e.g. key="mykey", namespace="myns"). This enables more efficient queries and proper namespace isolation.
The adapter automatically adds the namespace column and creates the appropriate index when it connects, so no manual schema changes are needed for new installations.
The adapter now extends Hookified instead of a custom EventEmitter. Events work the same (on, emit), but hooks are also available via the standard Hookified API.
v6 adds an expires BIGINT column to the table. When values are stored with a TTL via Keyv core, the adapter automatically extracts the expires timestamp from the serialized value and stores it in the column. A partial index is created on the expires column for efficient cleanup queries.
The schema migration is automatic on connect — existing tables get the column added via ADD COLUMN IF NOT EXISTS.
A new utility method that deletes all rows where the expires column is set and the timestamp is in the past:
await store.clearExpired();Set an interval (in milliseconds) to automatically call clearExpired() on a schedule. Disabled by default (0). The timer uses unref() so it won't keep the Node.js process alive.
const store = new KeyvPostgres({
uri: 'postgresql://user:pass@localhost:5432/dbname',
clearExpiredInterval: 60_000, // clean up every 60 seconds
});New methods for efficient multi-key operations:
.setMany(entries)— bulk upsert using PostgreSQLUNNEST.getMany(keys)— bulk retrieve usingANY.deleteMany(keys)— bulk delete usingANY.hasMany(keys)— bulk existence check
A convenience function to create a Keyv instance with KeyvPostgres as the store in one call:
import { createKeyv } from '@keyv/postgres';
const keyv = createKeyv({ uri: 'postgresql://user:pass@localhost:5432/dbname' });The iterator now uses cursor-based (keyset) pagination instead of OFFSET. This handles concurrent deletions during iteration without skipping entries and is more efficient for large datasets.
If you have existing data from v5, you need to run the migration script to move namespace prefixes from keys into the new namespace column. The script is located at scripts/migrate-v6.ts in the @keyv/postgres package.
Preview the changes first with --dry-run:
npx tsx scripts/migrate-v6.ts --uri postgresql://user:pass@localhost:5432/dbname --dry-runRun the migration:
npx tsx scripts/migrate-v6.ts --uri postgresql://user:pass@localhost:5432/dbnameYou can also specify a custom table, schema, and column lengths:
npx tsx scripts/migrate-v6.ts --uri postgresql://user:pass@localhost:5432/dbname --table cache --schema keyv
npx tsx scripts/migrate-v6.ts --uri postgresql://user:pass@localhost:5432/dbname --keyLength 512 --namespaceLength 512The migration runs inside a transaction and will roll back automatically if anything fails.
Important notes:
- The script only migrates rows where
namespace IS NULL. Rows that already have a namespace value (e.g. from a partial earlier migration) are skipped. - Keys are split on the first colon — the part before becomes the namespace, the rest becomes the key. Namespaces containing colons are not supported.
KeyvPostgres accepts a connection URI string or an options object. The options object accepts the following properties along with any PoolConfig properties from the pg library (e.g. max, idleTimeoutMillis, connectionTimeoutMillis):
| Option | Type | Default | Description |
|---|---|---|---|
uri |
string |
'postgresql://localhost:5432' |
PostgreSQL connection URI |
table |
string |
'keyv' |
Table name for key-value storage |
keyLength |
number |
255 |
Maximum key column length (VARCHAR length) |
namespaceLength |
number |
255 |
Maximum namespace column length (VARCHAR length) |
schema |
string |
'public' |
PostgreSQL schema name (created automatically if it doesn't exist) |
ssl |
object |
undefined |
SSL/TLS configuration passed to the pg driver |
iterationLimit |
number |
10 |
Number of rows fetched per batch during iteration |
useUnloggedTable |
boolean |
false |
Use a PostgreSQL UNLOGGED table for better write performance |
clearExpiredInterval |
number |
0 |
Interval in milliseconds to automatically clear expired entries (0 = disabled) |
All configuration options are exposed as properties with getters and setters on the KeyvPostgres instance. You can read or update them after construction.
Get or set the PostgreSQL connection URI.
- Type:
string - Default:
'postgresql://localhost:5432'
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname' });
console.log(store.uri); // 'postgresql://user:pass@localhost:5432/dbname'Get or set the table name used for storage.
- Type:
string - Default:
'keyv'
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname' });
console.log(store.table); // 'keyv'
store.table = 'cache';Get or set the maximum key length (VARCHAR length) for the key column.
- Type:
number - Default:
255
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', keyLength: 512 });
console.log(store.keyLength); // 512Get or set the maximum namespace length (VARCHAR length) for the namespace column.
- Type:
number - Default:
255
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', namespaceLength: 512 });
console.log(store.namespaceLength); // 512Get or set the PostgreSQL schema name. Non-public schemas are created automatically if they don't exist.
- Type:
string - Default:
'public'
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', schema: 'keyv' });
console.log(store.schema); // 'keyv'Get or set the SSL configuration for the PostgreSQL connection. Passed directly to the pg driver.
- Type:
object | undefined - Default:
undefined
const store = new KeyvPostgres({
uri: 'postgresql://user:pass@localhost:5432/dbname',
ssl: { rejectUnauthorized: false },
});
console.log(store.ssl); // { rejectUnauthorized: false }Get or set the number of rows to fetch per iteration batch.
- Type:
number - Default:
10
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', iterationLimit: 50 });
console.log(store.iterationLimit); // 50Get or set whether to use a PostgreSQL unlogged table for better write performance. Unlogged tables are faster but data is lost on crash.
- Type:
boolean - Default:
false
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', useUnloggedTable: true });
console.log(store.useUnloggedTable); // trueGet or set the interval in milliseconds between automatic expired-entry cleanup runs. When set to a value greater than 0, the adapter will automatically call clearExpired() at the specified interval. The timer uses unref() so it won't keep the Node.js process alive. Setting to 0 disables the automatic cleanup.
- Type:
number - Default:
0(disabled)
// Clean up expired entries every 60 seconds
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', clearExpiredInterval: 60_000 });
console.log(store.clearExpiredInterval); // 60000
// Disable it later
store.clearExpiredInterval = 0;Get or set the namespace for the adapter. Used for key prefixing and scoping operations like clear().
- Type:
string | undefined - Default:
undefined
const store = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname' });
store.namespace = 'my-namespace';
console.log(store.namespace); // 'my-namespace'Set a key-value pair. Returns true on success, false on failure.
key(string) - The key to set.value(any) - The value to store.ttl(number, optional) - Time to live in milliseconds.- Returns:
Promise<boolean>
await keyv.set('foo', 'bar');
await keyv.set('foo', 'bar', 5000); // expires in 5 secondsSet multiple key-value pairs at once using a single atomic PostgreSQL INSERT ... UNNEST ... ON CONFLICT statement. Each entry is a KeyvEntry<Value> object ({ key: string, value: Value, ttl?: number }), where Value is inferred from the entries provided. Returns a boolean[] indicating whether each entry was set successfully. Since the SQL statement is atomic, all entries either succeed (true) or all fail (false) together. On failure, an error event is emitted.
const results = await keyv.setMany([
{ key: 'foo', value: 'bar' },
{ key: 'baz', value: 'qux' },
]); // [true, true]Get a value by key. Returns undefined if the key does not exist.
const value = await keyv.get('foo'); // 'bar'Get multiple values at once. Returns an array of values in the same order as the keys, with undefined for missing keys.
const values = await keyv.getMany(['foo', 'baz']); // ['bar', 'qux']Check if a key exists. Returns a boolean.
const exists = await keyv.has('foo'); // trueCheck if multiple keys exist. Returns an array of booleans in the same order as the input keys.
await keyv.set('foo', 'bar');
await keyv.set('baz', 'qux');
const results = await keyv.hasMany(['foo', 'baz', 'unknown']); // [true, true, false]Delete a key. Returns true if the key existed, false otherwise.
const deleted = await keyv.delete('foo'); // trueDelete multiple keys at once. Returns a boolean[] indicating whether each key existed.
const results = await keyv.deleteMany(['foo', 'baz']); // [true, true]Clear all keys in the current namespace.
await keyv.clear();Utility helper method to delete all expired entries from the store. This removes any rows where the expires column is set and the timestamp is in the past. This is useful for periodic cleanup of expired data.
await keyv.clearExpired();Iterate over all key-value pairs. The iterator uses the namespace configured on the instance. Uses cursor-based pagination controlled by the iterationLimit property.
const iterator = keyv.iterator();
for await (const [key, value] of iterator) {
console.log(key, value);
}Disconnect from the PostgreSQL database and release the connection pool.
await keyv.disconnect();By default, the adapter creates a logged table. If you want to use an unlogged table for performance, you can pass the useUnloggedTable option to the constructor.
const keyvPostgres = new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', useUnloggedTable: true });
const keyv = new Keyv({ store: keyvPostgres });From the PostgreSQL documentation:
If specified, the table is created as an unlogged table. Data written to unlogged tables is not written to the write-ahead log (see Chapter 28), which makes them considerably faster than ordinary tables. However, they are not crash-safe: an unlogged table is automatically truncated after a crash or unclean shutdown. The contents of an unlogged table are also not replicated to standby servers. Any indexes created on an unlogged table are automatically unlogged as well.
If this is specified, any sequences created together with the unlogged table (for identity or serial columns) are also created as unlogged.
The adapter automatically uses the default settings on the pg package for connection pooling. You can override these settings by passing the options to the constructor such as setting the max pool size.
const keyv = new Keyv({ store: new KeyvPostgres({ uri: 'postgresql://user:pass@localhost:5432/dbname', max: 20 }) });You can configure SSL/TLS connections by passing the ssl option. This is passed directly to the underlying pg driver.
const keyvPostgres = new KeyvPostgres({
uri: 'postgresql://user:pass@localhost:5432/dbname',
ssl: {
rejectUnauthorized: false,
},
});
const keyv = new Keyv({ store: keyvPostgres });For more details on SSL configuration, see the node-postgres SSL documentation.
When testing you can use our docker compose postgresql instance by having docker installed and running. This will start a postgres server, run the tests, and stop the server:
At the root of the Keyv mono repo:
pnpm test:services:startTo just test the postgres adapter go to the postgres directory (storage/postgres) and run:
pnpm test