All articles

Guide

Implementing File System Access and OPFS in Android TWAs

September 25, 2026 · 8 min read

Local Storage Limits and the Need for Advanced File Systems

For developers converting high-performance web applications into Android apps via a Trusted Web Activity (TWA), managing files locally is a frequent engineering challenge. Traditional web storage solutions like localStorage or sessionStorage are limited to small, synchronous string transactions and suffer from strict quota constraints. While IndexedDB is excellent for structured queryable data, it is not optimized for handling massive binary objects, raw media files, or database files like SQLite databases.

To bridge this gap, modern browser engines have introduced the File System Access API and the Origin Private File System (OPFS). When your progressive web app is wrapped in a TWA, these modern APIs let you read and write files directly to the storage allocated to Chrome on the host Android device. This guide explains how to leverage OPFS and the broader File System Access API inside your TWA to deliver lightning-fast, native-quality file management.

Origin Private File System (OPFS) vs File System Access API

Before writing storage code, it is critical to distinguish between the standard File System Access API and the Origin Private File System. Both operate within your TWA, but they serve fundamentally different architectural purposes.

FeatureOrigin Private File System (OPFS)File System Access API (Public)
Target LocationPrivate, sandboxed origin storageUser-selected system folders (e.g. Documents, Downloads)
User PromptsNone (fully automatic and silent)Requires explicit file picker permission dialogs
Access SpeedExtremely fast, near-native direct disk I/OModerate speed due to cross-process validation
Data LifetimePersistent until app is uninstalled or clearedTemporary; permission resets on app restart
Web Worker SupportYes (supports synchronous Access Handles)Yes (asynchronous access only)

OPFS provides a private, highly optimized virtual file system that is completely isolated from other apps and the device's public folders. Files saved here are hidden from the user's generic Android file manager, making it the perfect choice for caching assets, storing internal application states, or executing heavy database operations. In contrast, the public File System Access API is ideal when your application needs to edit physical documents, export photos, or load external user files from the device storage.

Implementing the Origin Private File System in a TWA

To start using OPFS, you do not need to request native Android storage permissions in your app's manifest file. Because OPFS is sandboxed to your verified origin, the underlying browser engine manages the storage boundaries automatically.

You can access the root directory of your private file system using the navigator.storage.getDirectory() method. This asynchronous call returns a FileSystemDirectoryHandle object, which acts as the entry point for all file operations inside your TWA.

To create or open a file inside OPFS, use the getFileHandle method. This method accepts a filename string and an options object. Setting the create property to true ensures that the file is instantly initialized if it does not already exist:

const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('app_config.bin', { create: true });

Once you hold a valid file handle, you can read and write data using a writable stream. This asynchronous workflow is highly compatible with the main thread of your web app, ensuring that your user interface remains responsive and free of stutters during disk transactions:

const writable = await fileHandle.createWritable();
await writable.write(new TextEncoder().encode('Configuration data content'));
await writable.close();

Using the Synchronous API inside Web Workers

One of the most powerful features of OPFS is its synchronous execution mode, which is exclusively accessible inside Web Workers. Inside a worker thread, you can obtain a FileSystemSyncAccessHandle by calling createWritable()'s faster sibling, createSyncAccessHandle().

This synchronous access handle bypasses the standard asynchronous overhead of the web browser, allowing direct-to-disk binary operations. It is particularly valuable if you are porting high-performance C++ or Rust programs (compiled to WebAssembly) into your TWA, such as video encoders, audio editors, or custom database engines.

// This code runs inside a registered Web Worker
const root = await navigator.storage.getDirectory();
const fileHandle = await root.getFileHandle('sqlite_db.db', { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();

// Perform super-fast synchronous binary reads/writes
const buffer = new Uint8Array([0, 1, 2, 3]);
accessHandle.write(buffer);
accessHandle.flush();
accessHandle.close();

Using the File System Access API for User-Selected Files

When your TWA needs to interact with files that sit outside the app's private sandbox, such as importing an image from the user's camera roll or saving a PDF receipt to their public downloads folder, you must use the public File System Access API.

On Android, the File System Access API maps directly to the system's native Storage Access Framework (SAF). When you invoke window.showOpenFilePicker(), Chrome intercepts the call and launches the system file picker interface. This allows the user to securely grant access to specific files without exposing their entire storage device to your application.

async function selectUserDocument() {
try {
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: 'Images',
accept: { 'image/*': ['.png', '.gif', '.jpeg', '.jpg'] }
}]
});
const file = await fileHandle.getFile();
return file;
} catch (err) {
console.log('User cancelled or picker failed: ', err);
}
}

Note that on some mobile browsers, the File System Access API falls back to standard input elements (<input type="file">) if direct window methods are not fully supported. Your TWA code should always verify that window.showOpenFilePicker is defined before invoking it, falling back to traditional file inputs if necessary.

Data Longevity and Persistent Storage Requests

By default, files stored inside OPFS are classified as "best-effort" storage. If the Android host device runs critically low on physical disk space, the operating system or browser engine may automatically delete cached data to free up space, prioritizing system stability.

To prevent your critical application files from being cleared, you should explicitly request persistent storage status from the browser. Your TWA can check and request this status via the navigator.storage API:

if (navigator.storage && navigator.storage.persist) {
const isPersisted = await navigator.storage.persist();
if (isPersisted) {
console.log('Storage is marked as persistent and will not be cleared by the OS.');
} else {
console.log('Storage is temporary; prepare fallback cache structures.');
}
}

When a TWA is verified through Digital Asset Links and installed as a trusted package, Google Chrome is highly likely to grant the storage persistence request automatically. This ensures that your users do not lose their offline database records, draft files, or downloaded assets during normal system maintenance.

Ready to ship your Android app?

Paste your PWA URL, get a signed APK and a Google Play ready AAB in minutes.

Build my app