Back to Hub
SYSTEM ARCHITECTURE •

Your Browser Is a Database. Use It Like One.

Modern browsers ship a full storage stack: a private filesystem (OPFS), a transactional NoSQL database (IndexedDB), an HTTP cache you control (Cache Storage), and a background proxy (Service Workers) that decides what loads when the network dies. Used together, they let a web app work fully offline — no server required.

This guide walks the whole stack with runnable examples. And since NitroIDE itself is an offline-first PWA that persists your files locally, everything here mirrors patterns that work in production today.

OPFS: a real filesystem in the browser

The Origin Private File System gives your origin a private directory with near-native file performance — ideal for large assets, WASM modules, or virtual file systems:

const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('project.zip', { create: true });
const writable = await fileHandle.createWritable();
await writable.write(blob);
await writable.close();

Need synchronous, high-speed writes — e.g. streaming data from a worker? Sync access handles are the answer, but they only exist inside workers:

// Inside a dedicated worker:
const root = await navigator.storage.getDirectory();
const fh = await root.getFileHandle('data.bin', { create: true });
const access = await fh.createSyncAccessHandle();
access.write(buffer, { at: offset }); // synchronous, zero-copy
access.flush(); access.close();

IndexedDB: structured data with transactions

OPFS is for files; IndexedDB is for structured records — settings, document metadata, search indexes. It's asynchronous and transactional, which makes the raw API verbose. In practice, keep a tiny promise wrapper or use a micro-library; the key ideas are object stores and indexes:

const db = await new Promise((res, rej) => {
  const req = indexedDB.open('nitro', 1);
  req.onupgradeneeded = () => req.result.createObjectStore('files', { keyPath: 'path' });
  req.onsuccess = () => res(req.result);
  req.onerror = () => rej(req.error);
});
const tx = db.transaction('files', 'readwrite');
tx.objectStore('files').put({ path: '/index.html', mtime: Date.now() });
await new Promise((res, rej) => { tx.oncomplete = res; tx.onerror = rej; });

OPFS vs IndexedDB — which one? Files and binary blobs → OPFS (faster, streaming-friendly). Structured records, queries, and indexes → IndexedDB. Many real apps use both: file bytes in OPFS, metadata and search indexes in IndexedDB.

Service workers: your offline proxy

A service worker sits between your page and the network. Register it once, and it can serve cached responses when offline, prefetch strategically, and even mock APIs during development:

// sw.js — cache-first for app shell, network-first for data
self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (url.pathname.startsWith('/api/')) {
    // Mock API responses entirely offline — great for prototyping
    event.respondWith(new Response(
      JSON.stringify({ users: [{ id: 1, name: 'Ada' }] }),
      { headers: { 'Content-Type': 'application/json' } }
    ));
    return;
  }
  event.respondWith(
    caches.match(event.request).then(r => r || fetch(event.request))
  );
});

Two honest requirements: service workers need HTTPS (or localhost), and their scope is limited to their directory and below. They also can't intercept the very first page load that registers them.

Faster boots: navigation preload

Normally a service worker delays navigation while it boots. Navigation preload lets the browser fetch the page in parallel with worker startup:

// In the service worker's activate handler:
await self.registration.navigationPreload.enable();
// Then in the fetch handler, use the preloaded response:
const preload = await event.preloadResponse;
event.respondWith(preload || fetch(event.request));

Sync when the network returns

Background Sync lets you queue work while offline and have the browser retry it when connectivity returns — the classic "offline commit" pattern:

// Page: queue a sync when the user hits "save" offline
await navigator.serviceWorker.ready.then(reg =>
  reg.sync.register('upload-changes')
);
// sw.js: the browser fires this when back online
self.addEventListener('sync', (event) => {
  if (event.tag === 'upload-changes')
    event.waitUntil(uploadPendingChanges());
});

Coordinating tabs: Web Locks

Two tabs editing the same OPFS file is a race condition waiting to happen. The Web Locks API gives you mutexes across tabs and workers:

await navigator.locks.request('project-file', async (lock) => {
  // Exclusive access across every tab and worker — safe to write
  await writeFileSafely();
}); // lock auto-releases here

Client-side encryption with WebCrypto

If you store sensitive data locally, encrypt it. AES-GCM via WebCrypto is straightforward — the honest caveat is key management: a key stored next to the data it protects only defends against other origins and casual snooping, not against someone with full device access:

const key = await crypto.subtle.generateKey(
  { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt(
  { name: 'AES-GCM', iv }, key,
  new TextEncoder().encode(secretText)
);
// Store {iv, ct} in OPFS/IndexedDB. Derive the key from a
// password with PBKDF2 or HKDF instead of storing it raw.

The offline test that matters: open DevTools → Network → set "Offline", then reload your app. If it boots and your data is there, your storage layer works. If it shows a dinosaur, you have a service worker gap. NitroIDE passes this test — it's a PWA that keeps working on airplane mode.

Build Apps That Survive Airplane Mode.

Prototype offline-first frontends in a browser IDE that works offline itself.

Launch NitroIDE