I Built a Zero-Dependency KV Store That Works in Browser, Node.js AND Cloudflare Workers
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 ('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 ('visits'); } }; ✅ React Hook - Reactive KV State //ts import { useKV } from 'kv-one/react'; function ThemeToggle() { const { value: theme, setValue, loading } = useKV ('theme', 'dark'); if (loading) return Loading... ; return ( setValue(theme === 'dark' ? 'light' : 'dark')}> Current theme: {theme} ); } 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 ('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, **prototype **keys 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)
Comments
No comments yet. Start the discussion.