v6 storage adapters accept an absolute expires timestamp (Unix ms) on set / setMany. Pre-v6 adapters took a relative ttl. You do not need to rewrite those adapters: Keyv wraps them in KeyvBridgeAdapter.
The public API is unchanged. keyv.set(key, value, ttl) is still relative milliseconds.
Table of Contents
When the bridge is used
Keyv looks at the store in this order:
store.capabilities.expires === true→ use the adapter as-is (v6 contract).- Full async adapter without that flag →
KeyvBridgeAdapter(legacy relative TTL). - Map-like, with methods that aren't native
asyncfunctions →KeyvMemoryAdapter. If the store isn't aMap, Keyv first calls itshasonce, and if that returns a promise the store goes toKeyvBridgeAdapterinstead. That covers adapters compiled to an older target and methods written withoutasync. - Async Map-like →
KeyvBridgeAdapter. - Anything else →
'error'and fall back toKeyvMemoryAdapter(new Map()).
import Keyv, { KeyvBridgeAdapter } from "keyv";
const keyv = new Keyv({ store: myLegacyAdapter });
// equivalent to:
const explicit = new Keyv({
store: new KeyvBridgeAdapter(myAsyncStore),
namespace: "cache",
});
A namespace on the Keyv options is applied to the adapter. Without one, Keyv keeps the namespace the adapter was configured with.
What the bridge does
- Converts expiry — absolute
expires→ relativettlfor the wrappedset. If the deadline is already past, it deletes instead of writing. - Delegates batch methods —
getMany,setMany,has,hasMany,deleteMany,iterator,disconnectwhen present; otherwise loops over single-key methods. - Namespaces — if the store has a
namespaceproperty, the bridge assigns it and does not prefix keys (avoids double-prefixing). When the bridge has no namespace, the store keeps its own. Otherwise the bridge prefixesnamespace::key(set the separator withnamespaceSeparator), and a namespacedclear()finds those keys with the store'siterator(). A store withoutiterator()can't tell namespaces apart, soclear()throws instead of wiping every namespace. - Forwards
'error'from the wrapped store onto the bridge (and then onto Keyv).
Writing a v6 adapter instead
Prefer declaring the new contract so you skip the conversion and never parse encoded values to recover TTL (that used to fail under compression, encryption, or non-JSON serializers):
import { keyvStorageCapability, type KeyvStorageEntry } from "keyv";
class MyAdapter {
get capabilities() {
return keyvStorageCapability(this);
}
async set(key, value, expires) {
// expires is Unix ms, or undefined
}
async setMany(entries: KeyvStorageEntry[]) {
// each entry has absolute expires
}
}
See Storage Adapters and Detect Capabilities.
Third-party adapters
Community adapters that have not declared capabilities.expires keep working through the bridge. You can still pass { store: communityAdapter }. When you maintain an adapter, upgrading to the v6 contract is recommended.
The third-party list has community backends and a walkthrough for implementing KeyvStorageAdapter.