Storage & Databases

NANO provides three built-in storage layers. Pick the right one for your data shape.

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

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

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 | nullRead raw bytes. Returns null if key absent.
kv.set(key, value)voidWrite bytes or string. Overwrites silently.
kv.delete(key)voidRemove a key. No-op if absent.
kv.list(prefix)[string, Uint8Array][]All keys matching prefix, with their values.
kv.getJSON(key)any | nullRead and JSON.parse. Null if absent.
kv.setJSON(key, val)voidJSON.stringify and write.
openKV(name)KVStoreOpen 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

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 };
usage in your handler
// 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.

Reading config and serving static files
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.
Memory backend

Default. Files live in RAM, cleared on process exit. Fast for tests and ephemeral data.

Disk backend

Files persisted to the local filesystem under a configurable root path.

S3 backend

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.

PlanetScale / Neon / Turso — HTTP database clients
// 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 });
  },
};
Redis via Upstash REST API
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 / LibSQLSQLite (edge)@libsql/client/http (bundle with esbuild)
NeonPostgres (serverless)@neondatabase/serverless (HTTP mode)
PlanetScaleMySQL (serverless)@planetscale/database
Upstash RedisRedis (REST API)@upstash/redis or raw fetch()
Cloudflare D1SQLite (REST API)fetch() to CF API or @cloudflare/d1-rest
SupabasePostgres (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