# Fast File Storage With the Origin Private File System

Source: https://www.egnworks.com/blog/origin-private-file-system-explained  
Author: Jacob Val  
Published: 2026-09-24  
Updated: 2026-09-24  
Category: Frontend Architecture  
Tags: Origin Private File System, Web Storage

> A deep technical guide to the Origin Private File System, covering why IndexedDB was never a real file system, getting a handle into the private tree, synchronous access from a worker, reading and writing at a byte offset, exclusive locking, a SQLite in the browser example, and browser support.

---

A SQLite database compiled to WebAssembly, a video editor buffering large clips, or any application writing the same large blob of data repeatedly has never had a good storage option in the browser. IndexedDB is asynchronous and transactional by design, built around small object writes rather than byte level file operations, and every read or write pays the cost of that model regardless of whether the application actually needs a transaction at all. The Origin Private File System gives a web application something closer to an actual file system instead, private to its own origin and fast enough for the kind of workload IndexedDB was never built for.

## Why IndexedDB Was Never a Real File System

IndexedDB stores structured objects inside transactions, and every read or write goes through the browser's transaction machinery even for a single, simple value. Writing a large binary blob means serializing it into that object model, and updating part of a large file means reading the whole object back out, modifying it in memory, and writing the entire thing back in, since IndexedDB has no concept of writing to a specific byte offset inside an existing value. This is a reasonable model for an address book or a cache of API responses, and a genuinely poor fit for a database engine or any application that needs to modify part of a large file without rewriting the whole thing.

## What Makes a File "Private" to an Origin

The Origin Private File System is a storage area scoped to a single origin and never shown to the user through their operating system's file browser. There is no save dialog, no permission prompt, and no path on the user's actual disk that maps directly to a file living here, which is also what lets the browser skip the safety checks and antivirus scanning a user visible download normally goes through, since nothing in this tree is ever exposed to the user as a file they could open or run directly.

## Getting a Handle Into the Private Tree

`navigator.storage.getDirectory()` returns the root of an origin's private file system, and standard File System Access API methods navigate and create entries inside it from there.

```js
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle("app.db", { create: true });
```

This part of the API is asynchronous and works on the main thread the same way the broader File System Access API does, useful for setting things up, but not yet the part that makes OPFS fast.

## Synchronous Access From a Worker

The actual performance gain comes from `createSyncAccessHandle()`, available only inside a dedicated worker, which returns a handle supporting genuinely synchronous reads and writes rather than promise based ones.

```js
// inside a worker
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle("app.db", { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();

const size = accessHandle.getSize();
```

Everything up to creating the handle is still asynchronous, since it involves the file system's directory structure, but every operation performed through the resulting `accessHandle` runs synchronously, with no promise, no microtask queue, and no await needed for the read or write itself.

## Reading and Writing at a Byte Offset

`read()` and `write()` both accept an `at` option specifying the exact byte offset to operate on, which is the capability IndexedDB simply does not have.

```js
const buffer = new Uint8Array(1024);
accessHandle.read(buffer, { at: 4096 });

const update = new TextEncoder().encode("new data");
accessHandle.write(update, { at: 4096 });
```

A database engine can update one page of a much larger file in place this way, without reading the entire file into memory first or rewriting anything beyond the bytes that actually changed, which is exactly the operation a SQLite engine or any custom binary format performs constantly.

## Why Synchronous Access Requires a Worker

Genuinely synchronous file operations would block the main thread for as long as the underlying disk access takes, which is unacceptable on the thread responsible for rendering and responding to input. Restricting `createSyncAccessHandle()` to dedicated workers sidesteps this entirely, the worker's own thread can block without affecting the page's responsiveness, which is what makes true synchronous access viable here in a way it never could be on the main thread.

## Exclusive Locking on a Single Handle

Only one sync access handle can be open on a given file at a time, an attempt to open a second one while the first remains open fails rather than silently allowing concurrent access. This removes an entire category of race condition a shared, unlocked file would otherwise create, at the cost of an application needing to coordinate which worker currently owns a given file's handle, typically by keeping a single dedicated worker responsible for all access to a particular database file rather than letting multiple workers reach for it independently.

## Flushing and Closing

Writes through a sync access handle are not guaranteed to be durable until `flush()` is called, and the handle should be explicitly closed with `close()` once a worker is done with it, both to release the exclusive lock for anyone else waiting on it and to ensure any buffered writes are actually committed.

```js
accessHandle.flush();
accessHandle.close();
```

Forgetting to close a handle leaves the file locked for the remainder of that worker's lifetime, which is a common source of a second `createSyncAccessHandle()` call failing unexpectedly during development.

## A Practical Example

A SQLite database compiled to WebAssembly can use OPFS as its actual storage backend, treating the private file system exactly the way SQLite treats a file on a real disk.

```js
// worker.js
import sqlite3InitModule from "@sqlite.org/sqlite-wasm";

const sqlite3 = await sqlite3InitModule();
const db = new sqlite3.oo1.OpfsDb("/app.db");

db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)");
db.exec("INSERT INTO notes (body) VALUES (?)", { bind: ["first note"] });
```

The SQLite WebAssembly build handles all the byte offset reads and writes internally through the same sync access handle mechanism described above, giving a browser application an actual embedded relational database backed by real file storage rather than an object store pretending to be one.

## Comparing OPFS to IndexedDB and localStorage

| | localStorage | IndexedDB | OPFS |
| --- | --- | --- | --- |
| Access model | Synchronous, main thread only | Asynchronous, transactional | Synchronous, worker only |
| Byte level offset writes | No | No | Yes |
| Typical fit | Small key value pairs | Structured objects, moderate size | Large files, databases, frequent partial updates |
| Storage limit | A few megabytes | Subject to browser storage quota | Subject to browser storage quota |

## What OPFS Does Not Give You

OPFS is still subject to the same overall storage quota IndexedDB shares on a given origin, it does not grant unlimited disk space just because it behaves more like a real file system. It also remains strictly private to its origin, there is no API for sharing a file between two different origins, and nothing in the private tree is visible to the user or exportable as a normal download without an application explicitly building that export path itself, reading the file's bytes and offering them through a normal download mechanism.

## Browser Support

The Origin Private File System is supported in Chrome, Edge, Firefox, and Safari, and counts as broadly available today, making it a reasonable choice for a project that genuinely needs file level storage rather than an experimental feature to plan around carefully.

## Conclusion

The Origin Private File System gives a web application the one thing IndexedDB was never designed to provide, real byte level file access with genuinely synchronous reads and writes. `createSyncAccessHandle()`, restricted to a worker so it can block without freezing the page, is what makes this synchronous access possible at all, and the exclusive lock on each open handle is what keeps concurrent access from silently corrupting a file. For a SQLite database compiled to WebAssembly, or any application whose storage needs look more like a file than a collection of small objects, this is the storage layer IndexedDB was standing in for all along.

## References

[MDN: Origin Private File System](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system)

[MDN: FileSystemSyncAccessHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemSyncAccessHandle)

[web.dev: The Origin Private File System](https://web.dev/articles/origin-private-file-system)

[MDN: File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API)
