Storage & Databases
NANO provides three built-in storage layers. Pick the right one for your data shape.
Embedded EdgeStore key-value database. Persistent across restarts. Namespaced per app. Best for counters, sessions, feature flags, structured config.
Browser-compatible localStorage API built on nano:kv. Drop-in for front-end code and Cloudflare Workers patterns using localStorage.*.
Virtual File System for static assets, config files, and binary data. Memory or disk backend, swappable to S3. Per-app namespace isolation.
nano:kv — Embedded Key-Value Database
v2.2.2+
nano:kv wraps EdgeStore, a Rust embedded KV engine.
Data is stored in .nano-kv/ next to the binary and persists across restarts.
Every app gets its own namespace — hostname::kv_name::key — so tenants can never read each other's data.
Persistent counter
Source: examples/kv-counter.js
import { kv } from 'nano:kv';
export default {
async fetch(request) {
const raw = await kv.get('hits');
const hits = raw ? parseInt(new TextDecoder().decode(raw), 10) : 0;
const next = hits + 1;
await kv.set('hits', new TextEncoder().encode(String(next)));
return new Response(JSON.stringify({ hits: next }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
},
};
Multiple namespaces with JSON helpers
Source: examples/kv-namespaced.js
import { kv, openKV } from 'nano:kv';
// Each openKV() call creates an isolated namespace within this app
const cache = openKV('cache');
const sessions = openKV('sessions');
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/set' && request.method === 'POST') {
const body = await request.json();
await cache.setJSON(body.key, body.value); // JSON helper
return Response.json({ ok: true });
}
if (url.pathname === '/get') {
const value = await cache.getJSON(url.searchParams.get('key'));
return Response.json({ value });
}
if (url.pathname === '/list') {
const prefix = url.searchParams.get('prefix') ?? '';
const entries = await cache.list(prefix); // prefix scan
return Response.json(entries.map(([k, v]) => ({
key: k, value: new TextDecoder().decode(v),
})));
}
return new Response('Not Found', { status: 404 });
},
};
Full API
| Method | Returns | Description |
|---|---|---|
| kv.get(key) | Uint8Array | null | Read raw bytes. Returns null if key absent. |
| kv.set(key, value) | void | Write bytes or string. Overwrites silently. |
| kv.delete(key) | void | Remove a key. No-op if absent. |
| kv.list(prefix) | [string, Uint8Array][] | All keys matching prefix, with their values. |
| kv.getJSON(key) | any | null | Read and JSON.parse. Null if absent. |
| kv.setJSON(key, val) | void | JSON.stringify and write. |
| openKV(name) | KVStore | Open a named namespace. Same API as kv. |
Data directory
EdgeStore files live in .nano-kv/kv/ relative to the working directory. The engine is shared across all worker threads (one Mutex<Engine>); each app is isolated by key prefix.
localStorage — Browser-Compatible KV
NANO ships a userland localStorage shim built on nano:kv.
Copy examples/localStorage-shim.js into your app and import it once — after that, localStorage.setItem / getItem / removeItem / clear work exactly like in a browser.
Data is persisted via EdgeStore, not lost between requests.
Source: examples/localStorage-shim.js
import { openKV } from 'nano:kv';
const store = openKV('localStorage');
const localStorage = {
async getItem(key) {
const bytes = await store.get(String(key));
return bytes ? new TextDecoder().decode(bytes) : null;
},
async setItem(key, value) {
await store.set(String(key), new TextEncoder().encode(String(value)));
},
async removeItem(key) {
await store.delete(String(key));
},
async clear() {
const entries = await store.list('');
await Promise.all(entries.map(([k]) => store.delete(k)));
},
async length() {
return (await store.list('')).length;
},
async key(index) {
const entries = await store.list('');
return index < entries.length ? entries[index][0] : null;
},
};
globalThis.localStorage = localStorage;
export { localStorage };
// Import once at the top of your entry point
import './localStorage-shim.js';
export default {
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === '/set') {
const key = url.searchParams.get('key') ?? 'default';
const value = url.searchParams.get('value') ?? '';
await localStorage.setItem(key, value);
return Response.json({ ok: true, key, value });
}
if (url.pathname === '/get') {
const key = url.searchParams.get('key') ?? 'default';
const value = await localStorage.getItem(key);
return Response.json({ key, value });
}
return new Response('Use /set?key=k&value=v or /get?key=k');
},
};
Note: The shim methods are async (they return Promises), unlike the synchronous browser localStorage. This is intentional — storage I/O in an edge runtime must not block the event loop. Add await before every call.
VFS — Virtual File System
Nano.fs exposes an async file system API backed by the Virtual File System. The default backend is in-memory; a disk backend can be configured. For cloud deployments an S3-compatible backend is available via the vfs-s3 feature flag.
VFS is ideal for static assets, config files loaded at startup, and larger binary blobs where a KV key is not the right shape.
export default {
async fetch(request) {
const url = new URL(request.url);
// Read a JSON config bundled into the VFS
if (url.pathname === '/config') {
const raw = await Nano.fs.readFile('/data/config.json', 'utf-8');
const config = JSON.parse(raw);
return Response.json(config);
}
// Serve a static HTML file
if (url.pathname === '/') {
const html = await Nano.fs.readFile('/public/index.html', 'utf-8');
return new Response(html, {
headers: { 'content-type': 'text/html' },
});
}
// Write a log entry
if (url.pathname === '/log' && request.method === 'POST') {
const body = await request.text();
await Nano.fs.writeFile('/logs/access.log', body + '\n');
return Response.json({ ok: true });
}
return new Response('Not Found', { status: 404 });
},
};
Nano.fs API
| Method | Description |
|---|---|
| Nano.fs.readFile(path, enc?) | Read file. Returns string (with encoding) or Uint8Array. |
| Nano.fs.writeFile(path, data) | Write string or Uint8Array to path. |
| Nano.fs.exists(path) | Returns boolean. Does not throw. |
| Nano.fs.deleteFile(path) | Remove a file. |
| Nano.fs.listDir(path) | List directory contents (partial support). |
| require('fs').readFileSync() | Sync Node.js-compatible read from VFS. |
| require('fs').writeFileSync() | Sync Node.js-compatible write to VFS. |
Default. Files live in RAM, cleared on process exit. Fast for tests and ephemeral data.
Files persisted to the local filesystem under a configurable root path.
S3-compatible object storage via vfs-s3 feature. Replication-ready for multi-node deploys.
External Databases via fetch()
NANO does not support raw TCP sockets (net, pg native driver, etc.). For traditional databases, use an HTTP-based proxy or a serverless HTTP database client.
// Works with any HTTP-based database driver bundled via esbuild
import { Client } from '@libsql/client/http'; // Turso (LibSQL)
const db = new Client({
url: process.env.DATABASE_URL,
authToken: process.env.DATABASE_TOKEN,
});
export default {
async fetch(request) {
const { rows } = await db.execute(
'SELECT id, name FROM users WHERE active = 1 LIMIT 10'
);
return Response.json({ users: rows });
},
};
const UPSTASH_URL = process.env.UPSTASH_REDIS_REST_URL;
const UPSTASH_TOKEN = process.env.UPSTASH_REDIS_REST_TOKEN;
async function redisGet(key) {
const res = await fetch(`${UPSTASH_URL}/get/${key}`, {
headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` },
});
const { result } = await res.json();
return result;
}
async function redisSet(key, value, ttlSecs) {
await fetch(`${UPSTASH_URL}/set/${key}/${encodeURIComponent(value)}${ttlSecs ? `/ex/${ttlSecs}` : ''}`, {
headers: { Authorization: `Bearer ${UPSTASH_TOKEN}` },
});
}
export default {
async fetch(request) {
const cached = await redisGet('hot-data');
if (cached) return Response.json(JSON.parse(cached));
// ... fetch from origin, store in Redis
const data = { ts: Date.now() };
await redisSet('hot-data', JSON.stringify(data), 60);
return Response.json(data);
},
};
HTTP-compatible database services
| Service | Type | Driver pattern |
|---|---|---|
| Turso / LibSQL | SQLite (edge) | @libsql/client/http (bundle with esbuild) |
| Neon | Postgres (serverless) | @neondatabase/serverless (HTTP mode) |
| PlanetScale | MySQL (serverless) | @planetscale/database |
| Upstash Redis | Redis (REST API) | @upstash/redis or raw fetch() |
| Cloudflare D1 | SQLite (REST API) | fetch() to CF API or @cloudflare/d1-rest |
| Supabase | Postgres (REST) | @supabase/supabase-js (REST mode) |
Choosing the right storage
| Use case | Recommended | Why |
|---|---|---|
| Request counters, feature flags | nano:kv | Zero-latency local reads, no network hop |
| Session tokens, short-lived cache | nano:kv (openKV) | Named namespace keeps sessions isolated from other KV data |
| Browser-targeting code using localStorage | localStorage shim | Drop-in — no code changes, persistent via EdgeStore |
| Static assets, config files | Nano.fs (VFS) | Path-based access, binary blobs, bundled at deploy time |
| Relational data, complex queries | External (Turso/Neon) | SQL semantics, joins, transactions — use HTTP drivers |
| Global cache / pub-sub | External (Upstash Redis) | Cross-node visibility, TTL, pub-sub need network-accessible store |