native-file-system-adapter: one file system API across browsers, Node and Deno
File system, based on the spec reference implementation
At a glance
- What is it?
- This ponyfill implements the File System Access specification's API surface, including directory and file pickers, writable streams and an origin private directory, with pluggable backends for Node, Deno, IndexedDB, the Cache API, memory and IndexedDB-style storage. The point is that your code keeps working when the browser does not have the real API.
- Who is it for?
- native-file-system-adapter suits a team that wants to write file system code once against the File System Access specification and run it in browsers without that API as well as in Node and Deno, which is a real portability win for tooling that runs in both places. It does not suit you if you only target one modern browser with native support, because then the spec implementation is already there.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 101 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Five pieces of the spec, and one of them is the interesting one
native-file-system-adapter is a file system API that follows the File System Access specification, and the stated purpose is a unified way of handling data in all browsers and even in NodeJS and Deno in a more secure way.
The package is broken into several bits, and the list is worth reading because it shows how much of the spec surface is actually covered.
First, ponyfills for showDirectoryPicker, showOpenFilePicker and showSaveFilePicker, with fallbacks to regular input elements. Second, ponyfills for the FileSystemFileHandle and FileSystemDirectoryHandle interfaces. Third, a ponyfill for FileSystemWritableFileStream to truncate and write data. Fourth, an implementation of navigator.storage.getDirectory(), named getOriginPrivateDirectory, which can read and write data to and from several sources called adapters, not just the browser sandboxed file system. Fifth, a polyfill for DataTransferItem.prototype.getAsFileSystemHandle().
The fourth item is the one that matters most in practice. The pickers are the part a user notices, but the origin private directory is the part your application logic actually uses, and having it work against a Node filesystem or an IndexedDB store rather than only the browser sandbox is what makes the same code portable.
Two design constraints are stated. The API is designed so it can work with or without the ponyfill if you choose to remove or add this, and it is not trying to interfere with the changing spec by using other properties that may conflict with feature changes to the spec. The current minimum supported browser is the ones that can handle import and export, and some parts are lazy loaded when needed.
Adapters are the point, and they are not all durable
The adapter list is where the practical differences between environments show up.
When getOriginPrivateDirectory is called with no arguments, the browser's native sandboxed file system is used, exactly like calling navigator.storage.getDirectory(). Optionally, a file system backend adapter can be provided as the argument, and the ponyfill ships with a few backends built in.
The node adapter uses NodeJS's fs module. The deno adapter interacts with the filesystem using Deno. The sandbox adapter is deprecated and uses requestFileSystem, which is only supported in Chromium-based browsers using the Blink engine. The indexeddb adapter stores files into the browser's IndexedDB object database. The memory adapter stores files in memory, which makes it a temporary file store that clears when the user leaves the page. The cache adapter stores files with the browser's Cache API in request and response pairs.
And you can load in your own underlying adapter and get the same set of APIs.
Two of those carry explicit warnings in the code examples. The memory adapter is a fast in-memory file system but it is temporary by definition, so anything you write there is gone when the tab is closed. The cache adapter is annotated in the example as bad at mutating data, which is the fundamental mismatch: the Cache API is a request and response store, not a file system, so overwriting a file in place is awkward.
The node and deno adapters take a starting path as a second argument, so the same code can point at a real directory on disk, which is how you get a Node process to behave like a browser with the spec API.
Installing and importing, in the browser and in Node
The package installs from npm:
npm i native-file-system-adapterIn Node or a bundler, you import the entry point and an adapter:
import { getOriginPrivateDirectory } from 'native-file-system-adapter'
import indexedDbAdapter from 'native-file-system-adapter/lib/adapters/indexeddb.js'
import nodeAdapter from 'native-file-system-adapter/lib/adapters/node.js'
const dirHandle = await getOriginPrivateDirectory(indexedDbAdapter)
const nodeDirHandle = await getOriginPrivateDirectory(nodeAdapter, './real-dir')In the browser, ES import works from a CDN, and the same function is available with and without an adapter:
<script type="module">
import { getOriginPrivateDirectory } from 'https://cdn.jsdelivr.net/npm/native-file-system-adapter/mod.js'
// Get a directory handle for a sandboxed virtual file system
// same as calling navigator.storage.getDirectory()
const dirHandle1 = await getOriginPrivateDirectory()
// or use an adapter (see adapters table above for a list of available adapters)
const dirHandle2 = await getOriginPrivateDirectory(import('https://cdn.jsdelivr.net/npm/native-file-system-adapter/src/adapters/<adapterName>.js'))
</script>Note the two import paths, lib/adapters for the node style imports and src/adapters for the browser dynamic import in the CDN example. That difference is in the documented examples, so it is not a typo on your side when the two forms appear to disagree.
The package metadata explains the layout. The main field is src/es6.js, the module field is the same path, the package is an ES module with type set to module, the type definitions are at types/mod.d.ts, and the engines field asks for node 20 or newer. The build is rollup, and the type declarations are generated with tsc from a jsconfig.json in the prepublish step.
Version 3 dropped the conditional polyfill loading that 2.x relied on
The upgrade note between 2.x and 3.x is the one thing to read if you are coming from an older version, and it is a behavioural change rather than a cosmetic one.
Version 3 removed all top level await that conditionally loaded polyfills like WritableStream, DOMException, and Blob and File, on the reasoning that all current up to date environments have these built in globally on the globalThis namespace. The stated effects are that the file system adapter becomes lighter for people who want a smaller bundle, and that it supports newer engines.
If you still need to provide polyfills for older environments, you can provide your own polyfill and set it up with the config before any other script is evaluated:
import config from 'native-file-system-adapter/config.js'
// This is the default config that you could override as needed.
Object.assign(config, {
ReadableStream: globalThis.ReadableStream,
WritableStream: globalThis.WritableStream,
TransformStream: globalThis.TransformStream,
DOMException: globalThis.DOMException,
Blob: globalThis.Blob,
File: globalThis.File
})
// continue like normal.
import xyz from 'native-file-system-adapter'The ordering constraint is the important part. The config has to be set up before any other script is evaluated, which in a module graph means the config import has to come first, as it does here.
The release history is short. Version 3.0.0 was published on 2022-11-24 and 3.0.1 on 2024-02-26, with 2.0.3 in March 2022, and the last push to the repository was on 2026-07-14. The licence is MIT, and the project is by Jimmy Wärting.
The pickers, drag and drop, and how the project tests itself
The picker ponyfills are the part with the most obvious fallback logic, and the fallback is not a plain input element.
The polyfilled file input version will turn into a memory adapter, and the comment in the example says you will have read and write permission on the memory adapter, so you might want to transfer, that is copy, the handle to another adapter. The options carry a _preferredMethods array:
import { showDirectoryPicker, showOpenFilePicker } from 'native-file-system-adapter'
// The polyfilled (file input) version will turn into a memory adapter
// You will have read & write permission on the memory adapter,
// you might want to transfer (copy) the handle to another adapter
const [fileHandle] = await showOpenFilePicker({_preferredMethods: ['native', 'input'], ...sameOpts})
const dirHandle = await showDirectoryPicker({_preferredMethods: ['native', 'input'], ...sameOpts})So the caller states a preference order, native first and input as the fallback, and the same options object is passed through. The consequence for a user is that in a browser without the native API, picking a file gives you a handle into a temporary in-memory store rather than a handle to the real file on disk, which is a meaningful difference if you were planning to write back to it.
Drag and drop is handled by conditionally polyfilling DataTransferItem.prototype.getAsFileSystemHandle():
// DataTransferItem.prototype.getAsFileSystemHandle() is conditionally polyfilled
import 'native-file-system-adapter'
window.ondrop = async evt => {
evt.preventDefault()
for (let item of evt.dataTransfer.items) {
const handle = await item.getAsFileSystemHandle()
console.log(handle)
}
}The testing setup is the most interesting part of the repository, because this project is explicitly built on the spec reference implementation. The test script runs the WPT suite under Node by default, there are separate scripts for Node, Deno, serialization and the WPT suite in Deno, and the browser variants run under Playwright with project selectors for chromium, firefox and webkit. A fetch-wpt script pulls the test suite down, and the example directory holds a test.html, test.js and a service worker, with a script that serves the example over a local PHP server.
The homepage is a hosted example page at jimmywarting.github.io/native-file-system-adapter/example/test.html, and the repository has a specs/ directory, a playwright.config.js, a rollup.config.js and an old-tsconfig directory.
Editorial conclusion
native-file-system-adapter suits a team that wants to write file system code once against the File System Access specification and run it in browsers without that API as well as in Node and Deno, which is a real portability win for tooling that runs in both places. It does not suit you if you only target one modern browser with native support, because then the spec implementation is already there. Before you adopt it, read the adapter table rather than assuming the storage is durable, because the memory backend clears on navigation and the cache backend is described as bad at mutating data, and check the version, since 3.0 dropped the conditional polyfill loading that 2.x depended on.
Frequently asked questions
What does native-file-system-adapter provide?
Ponyfills for showDirectoryPicker, showOpenFilePicker and showSaveFilePicker with fallbacks to regular input elements, ponyfills for FileSystemFileHandle and FileSystemDirectoryHandle, a ponyfill for FileSystemWritableFileStream, an implementation of navigator.storage.getDirectory() called getOriginPrivateDirectory that works against several adapters, and a polyfill for DataTransferItem.prototype.getAsFileSystemHandle().
Which backends can native-file-system-adapter store files in?
The built-in adapters are node, deno, a deprecated sandbox adapter for Chromium Blink engines, indexeddb, memory which clears when the user leaves the page, and cache which uses the Cache API. You can also supply your own adapter and get the same set of APIs.
What changed between version 2 and version 3 of native-file-system-adapter?
Version 3 removed the top level await that conditionally loaded polyfills such as WritableStream, DOMException, Blob and File, since current environments have them on globalThis. That makes the bundle smaller and supports newer engines, and if you need those polyfills you can override the config before any other script is evaluated.
How do I use native-file-system-adapter with a real directory in Node?
Import the node adapter and pass it to getOriginPrivateDirectory with a starting path, for example getOriginPrivateDirectory(nodeAdapter, './real-dir'). The node adapter uses NodeJS's fs module underneath.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jimmywarting-native-file-system-adapter)Was this useful?