Directory.ExternalStorage in @capacitor/filesystem is documented as inaccessible on Android 11 and newer, and no permission brings it back. Code that wrote a PDF into Download/ in 2020 still compiles and fails at runtime on every current device. That is not a plugin bug, it is the storage model Android enforces for every app that targets Android 11 (API level 30) or higher.
This is the condensed version of our longer write-up, Android Scoped Storage in Capacitor Apps, Explained. One disclosure first. The Capacitor File Manager plugin in the code samples is ours, and it is part of Capawesome Insiders, a paid subscription. The Android behavior below applies either way.
What Android changed, and when
Scoped storage limits an app to its own directory on external storage plus the media files it created itself. Android made it the default for apps that target Android 10 (API level 29) and higher, so an app can no longer walk the shared volume and read whatever it finds there.
Android 10 still allowed an opt-out. An app could set android:requestLegacyExternalStorage="true" in its manifest and keep the old behavior. Android 11 (API level 30) ignores that flag as soon as your app targets Android 11, and the same page states that both WRITE_EXTERNAL_STORAGE and the privileged WRITE_MEDIA_STORAGE permission stop providing additional access at that target level.
You cannot sit this out. Since 31 August 2026, Google Play requires new apps and app updates to target Android 16 (API level 36) or higher, six releases past the point where the legacy flag became dead configuration.
The Download directory is the one everybody asks about. Android 11 removed it from what the Storage Access Framework hands out. The system picker still lists it, but Google's own test instructions describe the expected result as the directory showing up with "the action button associated with the directory grayed out". The internal storage root and the roots of reliable SD card volumes are blocked the same way. For a file that has to land there, use the MediaStore.Downloads collection instead.
What still works in @capacitor/filesystem
Most of the plugin is unaffected. Scoped storage restricts shared storage, not the app sandbox, so app-specific directories behave as before Android 10 and need no permission.
| Directory | Status on Android 11 and newer |
|---|---|
Directory.Data |
Works. Maps to getFilesDir(), private to the app. |
Directory.Cache |
Works. Maps to getCacheDir(), may be reclaimed by the system. |
Directory.Library |
Works. Maps to getFilesDir() on Android. |
Directory.External |
Works. App-specific directory on the shared volume. |
Directory.ExternalCache |
Works. App-specific cache on the shared volume. |
Directory.Documents |
Partly. Your app only sees files and folders it created itself. |
Directory.ExternalStorage |
Gone. Documented as not accessible on Android 11 or newer. |
Two issues in ionic-team/capacitor-filesystem have tracked this gap for years without a resolution. #28, asking the plugin to accommodate scoped storage, was opened on 30 December 2020. #37 asks which Directory member to use for which purpose now that half the enum behaves differently per Android version. Until one of them lands, treat @capacitor/filesystem as a sandbox API that stops at the boundary of shared storage.
Why MANAGE_EXTERNAL_STORAGE is the wrong fix
MANAGE_EXTERNAL_STORAGE is the "all files access" permission Android 11 introduced, and it does restore broad access to shared storage. It is also the declaration most likely to stall a Play Store review. Google Play has evaluated apps that declare it under a dedicated policy since May 2021, and Google's guidance is to request it only when the app cannot do its job through the Storage Access Framework or the MediaStore API.
The permitted uses are narrow and tied to core functionality. File managers, backup tools, anti-virus apps, and similar categories where browsing arbitrary files is the product. An expense app that drops receipts into a folder is not in that group, and reviewers treat document management as a use case the Storage Access Framework already covers. The permission does not widen the Storage Access Framework either, so it buys you nothing there.
How the Storage Access Framework replaces shared paths
The Storage Access Framework turns "give me a path" into "let the user grant me a folder". Your app fires an ACTION_OPEN_DOCUMENT_TREE intent, available since Android 5.0 (API level 21), and the user picks a directory in the system picker. What comes back is a tree URI covering that directory and everything beneath it, and nothing else.
That grant expires when the device restarts. Calling takePersistableUriPermission() with the read and write flags makes it survive reboots and app restarts, which turns a one-off picker result into a folder your app keeps using. Android drops it again if the document behind it is moved or deleted, and the user then has to pick a folder again.
Nothing in this flow needs a permission in your manifest. The user's choice in the system picker is the permission.
Persisting a folder with the File Manager plugin
The Capacitor File Manager plugin puts that flow behind two methods, so you never touch a tree URI or a permission flag yourself. Let the user pick a directory with pickDirectory() from the Capacitor File Picker plugin, then pass the result to persistDirectoryAccess(...).
import { FileManager } from '@capawesome-team/capacitor-file-manager';
import { FilePicker } from '@capawesome/capacitor-file-picker';
const pickExportFolder = async () => {
const result = await FilePicker.pickDirectory();
const { directory } = await FileManager.persistDirectoryAccess({
uri: result.path,
bookmark: result.bookmark,
});
return directory;
};
The bookmark value is the iOS half of the same idea, a security-scoped bookmark. Android ignores it, because the persisted tree URI already carries the grant.
Do not store the returned URI yourself, because it can change between app launches. Read the current list on startup with getPersistedDirectories() instead, which drops directories whose document no longer exists. Every method of the plugin accepts a persisted URI. Build the URI of a file inside the folder with getUri(...) by passing parentUri instead of directory, then copy, move or read as usual.
import { Directory, FileManager } from '@capawesome-team/capacitor-file-manager';
const exportReport = async (directoryUri: string) => {
const { uri: sourceUri } = await FileManager.getUri({
path: 'report.pdf',
directory: Directory.Cache,
});
const { uri: targetUri } = await FileManager.getUri({
path: 'exports/report.pdf',
parentUri: directoryUri,
});
const { uri } = await FileManager.copyFile({ uri: sourceUri, toUri: targetUri });
return uri;
};
Two things to watch. Constructing the URI of an entry that does not exist yet works only for path-structured document providers such as local device storage, so a cloud provider mounted into the picker may reject it. And copyFile(...) returns the URI of the created file, which differs from the one you requested if the provider renamed the file to avoid a collision.
Which directory or API should you use?
Pick by who owns the file and who needs to see it, not by which directory name sounds closest to the old one.
| What you need | What to use |
|---|---|
| App data that must survive updates and stay private | Directory.Data |
| Files you can regenerate at any time | Directory.Cache |
| Large app-owned files on the shared volume | Directory.External |
| A user-visible folder your app keeps using across launches |
pickDirectory() plus persistDirectoryAccess(...)
|
| A single file the user selects once |
pickFiles(...) from the File Picker plugin |
| Photos, videos or audio in the gallery | MediaStore collections (platform API) |
A download the user finds in Download/
|
The MediaStore.Downloads collection |
| Browsing arbitrary files across the device |
MANAGE_EXTERNAL_STORAGE, only as core functionality |
Conclusion
If your app writes outside its own sandbox on Android, stop hunting for a directory constant that still works. Grep the project for Directory.ExternalStorage and Directory.Documents, move those call sites to a picked and persisted directory, and keep Directory.Data and Directory.Cache for everything the user never has to see. That split holds for every Android version you still support, so you do the work once.
The full guide goes deeper on the permission history, and our Capacitor file handling guide covers reading, writing and sharing files without running out of memory. If your case does not fit a row of the table above, drop it in the comments.
Top comments (0)