Community adapters let you use backends beyond the official list. Any store that implements KeyvStorageAdapter (or a Map-like / legacy async store that Keyv can bridge) works.
Available Adapters
| Adapter | Description |
|---|---|
| @resolid/keyv-sqlite | SQLite storage adapter for Keyv |
| keyv-arango | ArangoDB storage adapter for Keyv |
| keyv-azuretable | Azure Table Storage/API adapter for Keyv |
| keyv-browser | Browser storage adapter including localStorage and indexedDB |
| keyv-cloudflare | Storage adapter for Cloudflare Workers KV |
| keyv-dynamodb | DynamoDB storage adapter for Keyv |
| keyv-file | File system storage adapter for Keyv |
| keyv-firestore | Firebase Cloud Firestore adapter for Keyv |
| keyv-lru | LRU storage adapter for Keyv |
| keyv-momento | Momento storage adapter for Keyv |
| keyv-mssql | Microsoft SQL Server adapter for Keyv |
| keyv-null | Null storage adapter for Keyv |
| keyv-s3fifo | S3-FIFO storage adapter for Keyv |
| keyv-upstash | Upstash Redis adapter for Keyv |
| quick-lru | Simple "Least Recently Used" (LRU) cache |
How to Contribute
- Build your adapter following
KeyvStorageAdapter(below) - Test with @keyv/test-suite
- Publish to npm with the
keyvkeyword - Open a PR adding a row to this page (
website/site/docs/storage-adapters/third-party.md)
PR title: docs: add [your-adapter-name] to third-party storage adapters. Add the row in alphabetical order.
Building a Storage Adapter
v6 adapters take an absolute expires (Unix ms) on set / setMany and declare capabilities.expires. Legacy relative-ttl adapters still work through KeyvBridgeAdapter.
import type { IEventEmitter } from "hookified";
import type { KeyvStorageCapability, KeyvStorageEntry, KeyvValue } from "keyv";
type KeyvStorageGetResult<Value> = KeyvValue<Value> | string | undefined;
type KeyvStorageAdapter = {
namespace?: string;
capabilities?: KeyvStorageCapability;
get<Value>(key: string): Promise<KeyvStorageGetResult<Value>>;
set(key: string, value: unknown, expires?: number): Promise<boolean>;
setMany<Value>(values: KeyvStorageEntry<Value>[]): Promise<boolean[] | undefined>;
delete(key: string): Promise<boolean>;
clear(): Promise<void>;
has(key: string): Promise<boolean>;
hasMany(keys: string[]): Promise<boolean[]>;
getMany<Value>(keys: string[]): Promise<Array<KeyvStorageGetResult<Value | undefined>>>;
deleteMany(keys: string[]): Promise<boolean[]>;
disconnect?(): Promise<void>;
iterator?<Value>(): AsyncGenerator<Array<string | Awaited<Value> | undefined>, void>;
} & IEventEmitter;
KeyvStoreAdapter is a deprecated alias of KeyvStorageAdapter.
Table of Contents
Required methods
| Method | Description |
|---|---|
get(key) |
Return the stored payload or undefined. |
set(key, value, expires?) |
Persist a value. expires is absolute Unix ms; omit for no expiry. |
getMany(keys) |
Return stored payloads in the same order as the keys. |
setMany(entries) |
Persist entries and return one result per entry. |
delete(key) |
Return true if the key existed. |
deleteMany(keys) |
Delete keys and return one result per key. |
clear() |
Delete keys in the current namespace. |
has(key) |
Return whether a live value exists. |
hasMany(keys) |
Return one existence result per key. |
Optional methods
disconnect and iterator are optional. The other methods above are part of the direct v6 adapter contract.
Minimal example
import { EventEmitter } from "events";
import {
keyvStorageCapability,
type KeyvStorageAdapter,
type KeyvStorageCapability,
type KeyvStorageEntry,
type KeyvStorageGetResult,
} from "keyv";
type StoredEntry = {
payload: unknown;
expires?: number;
};
class MyCustomStore extends EventEmitter implements KeyvStorageAdapter {
public namespace?: string;
private readonly data: Map<string, StoredEntry>;
public constructor(data = new Map<string, StoredEntry>()) {
super();
this.data = data;
}
public get capabilities(): KeyvStorageCapability {
return keyvStorageCapability(this);
}
private prefix(): string {
return this.namespace ? `${this.namespace}:` : "";
}
private storageKey(key: string): string {
return `${this.prefix()}${key}`;
}
private liveEntry(key: string): StoredEntry | undefined {
const storageKey = this.storageKey(key);
const entry = this.data.get(storageKey);
if (entry?.expires !== undefined && entry.expires <= Date.now()) {
this.data.delete(storageKey);
return undefined;
}
return entry;
}
public async get<Value>(key: string): Promise<KeyvStorageGetResult<Value>> {
return this.liveEntry(key)?.payload as KeyvStorageGetResult<Value>;
}
public async set(key: string, value: unknown, expires?: number): Promise<boolean> {
const storageKey = this.storageKey(key);
if (expires !== undefined && expires <= Date.now()) {
this.data.delete(storageKey);
return true;
}
const entry: StoredEntry =
expires === undefined ? { payload: value } : { payload: value, expires };
this.data.set(storageKey, entry);
return true;
}
public async setMany<Value>(entries: KeyvStorageEntry<Value>[]): Promise<boolean[]> {
return Promise.all(
entries.map(({ key, value, expires }) => this.set(key, value, expires)),
);
}
public async delete(key: string): Promise<boolean> {
return this.data.delete(this.storageKey(key));
}
public async deleteMany(keys: string[]): Promise<boolean[]> {
return Promise.all(keys.map((key) => this.delete(key)));
}
public async getMany<Value>(
keys: string[],
): Promise<Array<KeyvStorageGetResult<Value | undefined>>> {
return Promise.all(keys.map((key) => this.get<Value | undefined>(key)));
}
public async has(key: string): Promise<boolean> {
return this.liveEntry(key) !== undefined;
}
public async hasMany(keys: string[]): Promise<boolean[]> {
return Promise.all(keys.map((key) => this.has(key)));
}
public async clear(): Promise<void> {
const prefix = this.prefix();
if (!prefix) {
this.data.clear();
return;
}
for (const key of this.data.keys()) {
if (key.startsWith(prefix)) {
this.data.delete(key);
}
}
}
public async *iterator<Value>(): AsyncGenerator<
Array<string | Awaited<Value> | undefined>,
void
> {
const prefix = this.prefix();
for (const [storageKey, entry] of this.data) {
if (prefix && !storageKey.startsWith(prefix)) continue;
if (entry.expires !== undefined && entry.expires <= Date.now()) {
this.data.delete(storageKey);
continue;
}
const key = prefix ? storageKey.slice(prefix.length) : storageKey;
yield [key, entry.payload as Awaited<Value>];
}
}
}
export default MyCustomStore;
import Keyv from "keyv";
import MyCustomStore from "./my-custom-store.js";
const keyv = new Keyv({ store: new MyCustomStore(), namespace: "my-app" });
await keyv.set("foo", "bar");
Testing
npm install --save-dev vitest keyv @keyv/test-suite
import { keyvTestSuite, storageTestSuite } from "@keyv/test-suite";
import { Keyv } from "keyv";
import { test } from "vitest";
import MyCustomStore from "./my-custom-store.js";
const store = () => new MyCustomStore();
keyvTestSuite(test, Keyv, store);
storageTestSuite(test, store);
See the storage adapters overview and legacy adapters for the v6 expires contract and bridging.