# How Zakirullin Files Web App Uses OPFS for Offline File Handling

> Discover how Zakirullin Files leverages OPFS for robust offline file handling. Your data persists across sessions and remains accessible even without a network connection.

- Repository: [Artem Zakirullin/files.md](https://github.com/zakirullin/files.md)
- Tags: how-to-guide
- Published: 2026-05-21

---

**The Zakirullin Files application stores user files in the browser's Origin-Private File System (OPFS) to ensure data persists across page reloads and remains accessible without network connectivity, falling back to an in-memory file system when OPFS methods are unavailable.**

The zakirullin/files.md repository implements a markdown editor that leverages the **Origin-Private File System (OPFS)** to provide a robust offline experience. By storing documents directly in the browser's private file sandbox and pairing this with strategic IndexedDB handle persistence, the application maintains full functionality during network outages while ensuring user data survives browser sessions.

## Detecting OPFS Capability and Fallback Mechanisms

Before executing any file operations, the application verifies that the current browser fully supports the required OPFS API surface. This detection occurs in [`web/welcome.js`](https://github.com/zakirullin/files.md/blob/main/web/welcome.js) through the `opfsIsFullyUsable()` function.

### Browser Capability Checks

The function checks for the existence of both `FileSystemFileHandle.createWritable` and `FileSystemFileHandle.remove` methods. If either method is missing—common in Safari, older Chromium versions, or non-secure HTTP contexts—the application sets an internal flag and instantiates `MemFile` and `MemDir` classes to create a volatile in-memory filesystem.

```javascript
if (!opfsIsFullyUsable()) {
  console.warn('OPFS missing methods – using in‑memory FS');
  isMemFS = true;
  root = await getMemFSRoot();          // in‑memory fallback
} else {
  root = await navigator.storage.getDirectory(); // real OPFS root
}

```

### Graceful Degradation Strategy

Every OPFS-dependent function guards against missing API methods. When `navigator.storage.getDirectory()` throws an error or returns undefined, the code automatically delegates to `getMemFSRoot()`, ensuring the UI remains responsive even on unsupported browsers.

## Initializing the Origin-Private File System

On first load, when no user-selected folder exists, the application creates a temporary OPFS workspace seeded with default content.

### Temporary Storage Setup

The `getTemporaryStorageDirHandle()` function in [`web/welcome.js`](https://github.com/zakirullin/files.md/blob/main/web/welcome.js) obtains the OPFS root handle via `navigator.storage.getDirectory()` and populates it with markdown files defined in the `WELCOME_FILES` constant. This provides users with immediate, editable content while demonstrating the app's capabilities.

### Error Handling and HTTPS Requirements

If the browser lacks HTTPS or restricts storage access, the function catches these permission errors and triggers the in-memory fallback. This prevents initialization failures from crashing the application on incompatible environments.

## Persisting Directory Handles with IndexedDB

While OPFS provides the storage backend, **IndexedDB** handles the persistence of directory handles across browser sessions, eliminating the need for repeated permission prompts.

### Saving User Folder Selections

When a user opens a local folder through the file picker, the resulting `FileSystemDirectoryHandle` is serialized and stored in IndexedDB using `saveDirectoryHandle()` in [`web/app.js`](https://github.com/zakirullin/files.md/blob/main/web/app.js). This allows the application to maintain access to the same folder after page reloads.

### Retrieval Logic in getRootDirHandle

The `getRootDirHandle()` function orchestrates handle retrieval by first attempting to fetch the saved handle via `getSavedRootDirHandle()`. If the handle is unavailable or OPFS detection fails, the function delegates to `getTemporaryStorageDirHandle()` to create a fresh temporary workspace.

```javascript
async function getRootDirHandle() {
  // Try to get saved handle from IndexedDB first
  const saved = await getSavedRootDirHandle();
  if (saved && await verifyPermission(saved)) {
    return saved;
  }
  // Fall back to temporary OPFS storage
  return getTemporaryStorageDirHandle();
}

```

## Performing File Operations on OPFS

All high-level file system operations reside in [`web/lib/fs.js`](https://github.com/zakirullin/files.md/blob/main/web/lib/fs.js), providing a unified API that abstracts whether the backend is OPFS or in-memory.

### Core File System APIs

Functions like `read()`, `write()`, `createDir()`, and `remove()` obtain a directory handle through `getRootDirHandle()` and then use native OPFS methods including `getFileHandle()`, `createWritable()`, and `removeEntry()`. The `createDir()` function, for instance, creates a directory on OPFS while simultaneously mirroring the change in the in-memory tree used for UI rendering.

```javascript
async function savePage(path, content) {
  // `write` from lib/fs.js resolves the appropriate handle
  await write(path, content);   // writes via OPFS or memFS transparently
}

```

### Atomic Write Operations

The `write()` and `writeAtEnd()` functions utilize `FileSystemWritableFileStream` to perform atomic file updates. This prevents data corruption during write operations and ensures consistency between the OPFS storage and the application's internal state.

```javascript
await createDir('/projects/notes');   // OPFS directory + in‑memory entry
await renderSidebar();                // UI reflects new folder

```

## Caching Static Assets with Service Workers

While OPFS handles user-generated content, static application assets are managed separately by a Service Worker in [`web/offline.js`](https://github.com/zakirullin/files.md/blob/main/web/offline.js).

### Pre-caching Application Shell

During the `install` event, the Service Worker caches the HTML, CSS, JavaScript, and font files required to render the interface. This ensures the application shell loads instantly from cache regardless of network status.

```javascript
self.addEventListener('install', e => {
  e.waitUntil(caches.open('files-md-v1')
    .then(cache => cache.addAll(['/','/app.js','/lib/hypermd.js'])));
});

```

### Separation of Concerns

This architecture isolates user data (stored in OPFS) from application code (cached by the Service Worker). The OPFS provides writable, persistent storage for markdown files, while the Cache API delivers the static resources necessary to boot the application in offline scenarios.

## Summary

- **Capability Detection**: [`web/welcome.js`](https://github.com/zakirullin/files.md/blob/main/web/welcome.js) checks for `createWritable` and `remove` methods via `opfsIsFullyUsable()` before utilizing OPFS features.
- **Temporary Storage**: `getTemporaryStorageDirHandle()` initializes OPFS with welcome files when no user folder is selected.
- **Handle Persistence**: IndexedDB stores directory handles via functions in [`web/app.js`](https://github.com/zakirullin/files.md/blob/main/web/app.js) to maintain access across sessions without repeated permission prompts.
- **File Operations**: [`web/lib/fs.js`](https://github.com/zakirullin/files.md/blob/main/web/lib/fs.js) provides a unified API that writes to OPFS while maintaining an in-memory mirror for UI responsiveness.
- **Graceful Fallback**: Unsupported browsers automatically use `MemFile` and `MemDir` classes instead of native OPFS.
- **Asset Caching**: The Service Worker in [`web/offline.js`](https://github.com/zakirullin/files.md/blob/main/web/offline.js) caches static resources independently of user file storage.

## Frequently Asked Questions

### What browsers support the OPFS implementation in Zakirullin Files?

The application requires browsers implementing both `FileSystemFileHandle.createWritable` and `FileSystemFileHandle.remove` methods. This includes modern Chromium-based browsers and recent Firefox versions supporting the File System Access API. Safari and legacy browsers automatically fall back to the in-memory file system implemented in [`web/welcome.js`](https://github.com/zakirullin/files.md/blob/main/web/welcome.js).

### How does the app prevent data loss when OPFS is unavailable?

When `opfsIsFullyUsable()` returns false, the application instantiates `MemFile` and `MemDir` classes to create a volatile in-memory filesystem. While this data disappears on page reload, it ensures the application remains functional and allows users to export their work via download prompts before closing the browser.

### Why does the app use IndexedDB alongside OPFS?

IndexedDB stores `FileSystemDirectoryHandle` objects via `saveDirectoryHandle()` because OPFS handles themselves are not automatically persisted across browser sessions. This storage mechanism enables the app to reopen previously selected folders in [`web/app.js`](https://github.com/zakirullin/files.md/blob/main/web/app.js) without requiring users to re-grant permission through file picker dialogs.

### What is the difference between OPFS storage and the Service Worker cache?

OPFS stores user-generated markdown files and media content in a private, origin-specific directory using `navigator.storage.getDirectory()`, while the Service Worker in [`web/offline.js`](https://github.com/zakirullin/files.md/blob/main/web/offline.js) caches the application's static assets using the Cache API. OPFS provides writable, persistent file storage that survives browser restarts, whereas the Service Worker cache is read-only and stores only the application shell required for offline booting.