The Problem I Kept Running Into
Every time I start a new project, I write the same boilerplate. Need to cache something in the browser? LocalStorage. On the server? Write a Map wrapper. On Cloudflare Workers? Completely different API again.
// Browser
const get = (key) => {
if (typeof window === 'undefined') return null;
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
};
// Node.js — completely different
const cache = new Map();
const get = (key) => cache.get(key) ?? null;
// Cloudflare Workers — different again
const get = async (key) => {
const val = await env.MY_KV.get(key);
return val ? JSON.parse(val) : null;
};
Three runtimes. Three different APIs. Zero TTL support. Zero type safety. And I haven't even added error handling yet.
I got tired of this. So I built kv-one
One API. Every Runtime.
//ts
import { kv } from 'kv-one';
// Works identically in Browser, Node.js, and Cloudflare Workers
await kv.set('user', { name: 'Alice', role: 'admin' });
const user = await kv.get<{ name: string; role: string }>('user');
That's it. No configuration. The library detects your runtime automatically and picks the right storage backend.
What Makes It Different
I checked the existing packages before building this. Most of them have one or more of these problems:
Synchronous API : blocks the event loop, terrible for servers.
No TTL support : you can't expire data automatically
No Cloudflare Workers support : the fastest growing edge runtime is just... missing
Runtime dependencies : adding idb-keyval, sqlite, etc. to your bundle
Silent error swallowing : failures disappear, debugging becomes a nightmare
**kv-one **solves all of these.
Key Features
✅ Fully Async — Never Blocks the Event Loop
// ts
// All operations are Promise-based
const value = await kv.get('key');
await kv.set('key', value);
await kv.delete('key');
✅ TTL — Built-in Automatic Expiry
//ts
// Expires in 1 hour — works on ALL runtimes
await kv.set('otp', '482910', { ttl: 3600 });
// Returns null after expiry — automatically
const otp = await kv.get('otp');
TTL is implemented at the library layer — not delegated to native storage. So it works uniformly whether you're on localStorage, Memory, or Cloudflare KV.
✅ Namespacing — Perfect for Multi-Tenant Apps
//ts
const userStore = kv.namespace('users');
const sessionStore = kv.namespace('sessions');
await userStore.set('alice', { role: 'admin' });
await sessionStore.set('alice', { token: 'abc123' });
// Stored as 'users:alice' and 'sessions:alice' — no collisions
const keys = await userStore.keys(); // ['alice'] — scoped!
✅ Cloudflare Workers — First-Class Support
//ts
// workers.ts
import { createKV, CloudflareAdapter } from 'kv-one';
export default {
async fetch(request: Request, env: Env) {
const store = createKV({
adapter: new CloudflareAdapter({ namespace: env.MY_KV })
});
await store.set('visits', count, { ttl: 86400 });
const visits = await store.get<number>('visits');
}
};
✅ React Hook — Reactive KV State
//ts
import { useKV } from 'kv-one/react';
function ThemeToggle() {
const { value: theme, setValue, loading } = useKV<string>('theme', 'dark');
if (loading) return <span>Loading...</span>;
return (
<button onClick={() => setValue(theme === 'dark' ? 'light' : 'dark')}>
Current theme: {theme}
</button>
);
}
SSR-safe. Returns **defaultValue **on the server without touching the adapter.
✅ Zero Runtime Dependencies
npm i kv-one
That's it. Nothing else is installed. No idb-keyval, no better-sqlite3, no bloat. 31 KB total package size.
✅ Typed Error Classes
//ts
import { KVError, TTLError, KeyValidationError, AdapterError } from 'kv-one';
try {
await kv.set('my key', value); // space in key name
} catch (err) {
if (err instanceof KeyValidationError) {
console.log('Invalid key:', err.message);
}
}
No more debugging silent failures.
Or override it explicitly for full control:
//ts
import { createKV, MemoryAdapter, LocalStorageAdapter } from 'kv-one';
// Force in-memory (great for tests)``
const testStore = createKV({ adapter: new MemoryAdapter() });
// Force localStorage with a namespace prefix
const appStore = createKV({
adapter: new LocalStorageAdapter(),
prefix: 'myapp'
});
Quick Start
npm install kv-one
//ts
import { kv } from 'kv-one';
// Basic usage
await kv.set('counter', 0);
const count = await kv.get<number>('counter');
await kv.delete('counter');
// With TTL (seconds)
await kv.set('session', token, { ttl: 1800 }); // 30 minutes
// Namespaced
const cache = kv.namespace('cache');
await cache.set('posts', posts, { ttl: 300 });
await cache.clear(); // only clears cache:* keys
// Check existence
const exists = await kv.exists('session');
// List all keys
const keys = await kv.keys(); // ['session']
Security Built In
Security was a primary concern from the start:
Prototype pollution blocked: custom JSON reviver strips proto, constructor, prototypekeys
Circular reference detection: serializer catches them before they crash your app
Key validation: rejects keys with spaces, control characters, or dangerous patterns
Storage quota errors: surfaced as typed **AdapterError **instead of silent failures
📦 npm: npmjs.com/package/kv-one
🐙 GitHub: github.com/kumarprincepk/kv-one
If this saves you from writing the same localStorage boilerplate one more time, leave a ⭐ on GitHub — it really helps!
What runtimes are you targeting in 2026? Drop a comment below 👇
Top comments (0)