Understanding the File System Access API
A practical guide to the File System Access API: reading and writing local files from the browser, streaming large files to disk, its security model, and its real 2026 limits.
For most of the web's history, a browser tab could not touch your real files. It could receive an upload you explicitly chose, and it could hand the operating system a download that then got saved somewhere. Everything in between — opening a file, editing it in place, and writing the result back to the same location — was off limits. The File System Access API changes that. With your permission, a web page can open files and folders on your actual disk and read or write them directly, rather than treating every file as a one-shot upload or download.
This matters beyond convenience. One of the most consequential use cases is large files: with this API a page can stream tens of gigabytes straight to disk as the bytes arrive, without ever holding the whole file in memory. That single capability removes a ceiling that browser-based file tools used to hit constantly. This article explains what the API is, its core methods, why streaming changes the picture for big downloads, how the security model is built, and — just as importantly — where it still falls short in 2026.
What it actually is
The File System Access API is a set of JavaScript methods and objects that give a web page mediated access to the user's local file system. "Mediated" is the key word: the page never receives a path string or free rein over the disk. Instead it receives handles — objects (FileSystemFileHandle, FileSystemDirectoryHandle) that represent a specific file or folder the user picked, and nothing else.
A handle behaves like a capability. If the user grants access to one file, the page can read and write that file, but it cannot enumerate the rest of the folder, guess sibling paths, or escape upward to a parent directory. This is a deliberate contrast with native apps, which usually run with the full authority of your user account.
The core methods
There are three entry points, all exposed on the global window object, plus one writable-stream object used for writes.
showOpenFilePicker()— opens the OS file dialog and resolves to an array ofFileSystemFileHandleobjects for the files the user chose. Callawait handle.getFile()on one to get a normalFile(a kind ofBlob) you can read.showSaveFilePicker()— prompts the user for a save location and filename, resolving to a singleFileSystemFileHandleyou can then write to.showDirectoryPicker()— resolves to aFileSystemDirectoryHandlefor a whole folder, which you can iterate (for await (const entry of dirHandle.values())) and within which you can create or open child files.FileSystemWritableFileStream— the writable stream you obtain fromawait handle.createWritable(). Youwrite()chunks to it andclose()it when done, and the data lands on disk. Note thatcreateWritable()truncates the existing file to zero length by default; pass{ keepExistingData: true }if you intend to patch in place.
A minimal read looks like this: const [handle] = await window.showOpenFilePicker(); const file = await handle.getFile(); const text = await file.text();. A minimal write is the mirror image: const handle = await window.showSaveFilePicker(); const w = await handle.createWritable(); await w.write(blob); await w.close();. The close() call is what flushes the buffered writes to the real file, so forgetting it leaves the file empty or partial.
Why this matters for big downloads
To appreciate the streaming write, recall how downloads used to work in pure JavaScript. The classic trick is to build a Blob in memory, call URL.createObjectURL() on it, and click a hidden <a download> element. It works, but the entire file must exist in RAM first. A 5 GB export means roughly 5 GB of memory, and browsers will typically kill the tab well before that. The exact ceiling varies by browser, platform, and device RAM rather than being a fixed number, but in practice large multi-gigabyte blobs are unreliable, and Blob/ArrayBuffer sizes also run into the platform's maximum allocation limits.
FileSystemWritableFileStream removes that constraint. You open a writable stream to the destination file once, then push chunks into it as they arrive — for example, piping a fetch response body straight through: await response.body.pipeTo(await handle.createWritable()). Each chunk is written to disk and released, so memory usage stays roughly flat regardless of file size. On a machine with modest RAM you can save a file far larger than memory, because the file itself never lives in memory all at once.
Before this API existed, the common community workaround was StreamSaver.js, which uses a service worker to emulate a streaming download: it registers a worker that intercepts a request to a generated URL and returns a streamed response, letting the browser's own download machinery write to disk. It runs in more browsers, but it is fragile — it depends on service-worker lifecycle behavior, generally requires HTTPS (a service worker prerequisite), and breaks in subtle ways across browser updates. The native API does the same job without those workarounds, where it is supported. This streaming-to-disk mechanism is what browser-based tools rely on when they move large files locally without buffering everything in memory.
| Approach | Holds whole file in memory? |
|---|---|
Blob + <a download> | Yes — entire file buffered in RAM |
| StreamSaver.js (service worker) | No — streams via a service worker shim |
FileSystemWritableFileStream | No — chunks written straight to disk |
The security model
Direct disk access from a web page is dangerous if done carelessly, so the API is wrapped in several layers of protection. Understanding them matters, because most of the API's "why won't this work?" friction comes from these rules rather than from bugs.
Secure context and user gesture
The API is only available in a secure context — HTTPS, or http://localhost during development. It is also gated behind a user gesture: you cannot call showOpenFilePicker() on page load or from a timer. It must run inside the handler for a real user activation, such as a click or keypress. This prevents pages from silently popping file dialogs the moment they load.
Permission prompts and opaque handles
Picking a file through the open picker grants read access; writing usually triggers an additional permission prompt the first time. Handles are opaque in the sense that they expose no real filesystem path to the page, which keeps the user's directory structure private. You can persist a handle in IndexedDB to reopen the same file later, but re-access still requires the user to re-grant permission via requestPermission() (which prompts when needed); the grant does not silently persist forever across sessions.
Blocked paths
Browsers refuse access to sensitive locations. System directories, the browser's own profile directory, and other well-known dangerous paths are blocklisted, so a malicious page cannot trick you into handing over, say, your SSH key directory through the normal picker. The blocklist is enforced by the browser, not the page, so it cannot be bypassed by the site.
The Origin Private File System (OPFS)
There is a second, quieter half of the broader File System API: the Origin Private File System. Reached via navigator.storage.getDirectory(), OPFS is a private, sandboxed file system scoped to your site's origin. It needs no picker and no permission prompt because the user's real files are never involved — it is fast, origin-scoped scratch space, invisible to the user and to other origins. It suits things like a local database, a Wasm app's working files, or a download buffer. Crucially, OPFS is supported in far more browsers than the user-facing picker methods, which makes it a useful fallback target. Inside a Web Worker it also offers synchronous access handles (createSyncAccessHandle()), which is what high-performance Wasm databases use.
Browser support reality in 2026
This is where honesty matters. The user-facing picker methods are not universally available, and the situation has been stable for years.
- Chromium desktop browsers (Chrome, Edge, Opera, Brave, Arc, and so on) support the full picker API on desktop — open/save/directory pickers and streaming writes.
- Chromium on Android does not expose the pickers, because Android lacks a system picker that maps cleanly onto the API. So "Chromium" support effectively means Chromium on desktop, not on mobile.
- Firefox does not ship
showOpenFilePicker()/showSaveFilePicker()/showDirectoryPicker()on any platform; Mozilla has flagged the local-disk pickers as harmful. It does support OPFS. - Safari / WebKit supports OPFS (and synchronous access handles in workers) but does not offer the user-facing open/save/directory pickers on macOS, iPadOS, or iOS.
The practical rule: feature-detect, never assume. Check if ('showSaveFilePicker' in window) and fall back to the Blob-and-anchor download (or StreamSaver) when it is missing. For OPFS, check navigator.storage?.getDirectory. Treat the streaming picker path as a progressive enhancement that lights up on Chromium desktop and degrades gracefully everywhere else, including on Android.
What it does NOT give you
It is easy to over-read what this API enables, so here are the hard limits — and the cases where it simply does not apply.
- It is not full disk access. You only ever touch files and folders the user explicitly picks. There is no path enumeration, no "open this absolute path" call, and no background scanning.
- It does not work everywhere. As above, the pickers are effectively Chromium-desktop only in 2026. A cross-browser product, or anything targeting mobile, still needs a fallback path.
- Permissions are not permanent or silent. Even a persisted handle typically requires re-granting in a new session. You cannot read or write a user file in the background without a fresh grant.
- It needs HTTPS and a user gesture. No secure context, no API; no user activation, no picker. This rules out calling it from page load, timers, or non-secure origins.
- It is not a sync engine or a network tool. It moves bytes between a stream and the local disk. It does not encrypt, transfer over the network, resolve conflicts, or do anything beyond local read/write. Those concerns are yours to build with other tools.
- OPFS files are not user files. OPFS storage is invisible in the user's file explorer and can be wiped when the browser clears site data. Do not treat it as durable, user-facing storage.
Used within those boundaries, the File System Access API is one of the more genuinely useful additions to the web platform in recent years — it lets browser apps handle large files more like native software, without the memory ceiling that defined the old Blob era. The pragmatic approach is to write the cross-browser fallback first and let the streaming picker path be the upgrade where it is available.
Frequently asked questions
We build a real-time file-transfer relay in Rust, and write these articles from hands-on work with file transfer, networking, encryption, and browser platform APIs. More about us →