Third-Party Storage Adapters

Community storage adapters and how to implement KeyvStorageAdapter.

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

  1. Build your adapter following KeyvStorageAdapter (below)
  2. Test with @keyv/test-suite
  3. Publish to npm with the keyv keyword
  4. 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.

Edit this page