---
title: Android Scoped Storage in Capacitor Apps, Explained
description: Why Directory.ExternalStorage fails on Android 11 and how Capacitor scoped storage works with SAF tree URIs and persisted folder access instead.
date:
  created: 2026-09-08
  updated: 2026-09-08
authors:
  - robingenz
categories:
  - Capacitor
  - Guides
  - SDKs
links:
  - Capacitor File Manager: sdks/capacitor/file-manager.md
faq: true
---

# Android Scoped Storage in Capacitor Apps, Explained

`Directory.ExternalStorage` in `@capacitor/filesystem` is documented as inaccessible on Android 11 and newer, and no permission brings it back. Capacitor scoped storage 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 guide covers what Android changed, which directories still work without any permission, why `MANAGE_EXTERNAL_STORAGE` is the wrong fix, and how the Storage Access Framework replaces the shared paths your app used to write to. The [Capacitor File Manager plugin](../../sdks/capacitor/file-manager.md) wraps that framework in persisted folder access, so a folder the user picks once stays readable and writable across app launches. It is part of Capawesome Insiders, a paid subscription.

<!-- more -->

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Build and deploy your Capacitor app with Capawesome Cloud" src="https://capawesome.io/assets/banners/cloud-build-and-deploy-capacitor-apps.png?t=1" />
  </a>
</div>

## Key Takeaways

- Apps targeting Android 10 (API level 29) get scoped storage by default. `android:requestLegacyExternalStorage="true"` opted out of it, and Android ignores that flag once the app targets Android 11 (API level 30).
- The `@capacitor/filesystem` documentation states that `Directory.ExternalStorage` is not accessible on Android 11 or newer, and that `Directory.Documents` only exposes the files and folders your own app created.
- `WRITE_EXTERNAL_STORAGE` and the privileged `WRITE_MEDIA_STORAGE` grant no additional access to apps targeting Android 11 or higher.
- Google Play has evaluated `MANAGE_EXTERNAL_STORAGE` declarations against a dedicated policy since May 2021, and only apps whose core functionality needs all-files access qualify.
- The supported replacement is a directory tree URI from `ACTION_OPEN_DOCUMENT_TREE` combined with `takePersistableUriPermission()`, which keeps access across device restarts.
- App-specific directories still work without any permission on every Android version, including `Directory.Data`, `Directory.Cache`, `Directory.Library` and `Directory.External`.

## What is scoped storage, and when did Android enforce it?

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)](https://developer.android.com/training/data-storage#scoped-storage){:target="_blank"} and higher, so an app can no longer walk the shared volume and read whatever it finds there.

Android 10 left an escape hatch. An app could set `android:requestLegacyExternalStorage="true"` in its manifest and keep the old behavior. That hatch closed one release later. [Android 11 (API level 30)](https://developer.android.com/about/versions/11/privacy/storage){:target="_blank"} ignores the 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.

There is no way to sit out the change. Since 31 August 2026, [Google Play requires](https://developer.android.com/google/play/requirements/target-sdk){:target="_blank"} new apps and app updates to target Android 16 (API level 36) or higher, which is six releases past the point where the legacy flag became dead configuration. Any Capacitor app that ships through the Play Store runs under scoped storage.

## Why can't my Capacitor app write to the Android Downloads folder anymore?

Your app cannot write there because `Directory.ExternalStorage` points at the primary shared storage volume, and scoped storage closed that volume to ordinary apps. The [`@capacitor/filesystem` documentation](https://github.com/ionic-team/capacitor-filesystem){:target="_blank"} says so directly: the directory "is not accessible on Android 10 unless the app enables legacy External Storage" and "is not accessible on Android 11 or newer". Code that wrote a PDF to `Download/` in 2020 and still compiles today fails at runtime on every current device.

Requesting the storage permissions does not help either. The plugin only ever needed `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` for `Directory.Documents` and `Directory.ExternalStorage` on Android 10 and older, and those permissions grant nothing extra once the app targets Android 11.

The `Download` directory is a special case even under the new rules. Android 11 removed it from what the Storage Access Framework can hand out. The system picker still lists it, but Google's [own test instructions](https://developer.android.com/about/versions/11/privacy/storage){:target="_blank"} describe the expected result as the directory appearing with "the action button associated with the directory grayed out". The root of the internal storage volume and the roots of reliable SD card volumes are blocked in the same way.

That leaves three routes for a file the user is supposed to find outside your app:

- Write it into the `MediaStore.Downloads` collection. On Android 10 and higher, no storage permission is needed for files your own app creates there. The collection does not exist on Android 9 (API level 28) and lower.
- Ask the user to pick a folder, then keep access to it. This is the Storage Access Framework route, covered below.
- Keep the file in your sandbox and hand it to another app on demand with the [Capacitor File Opener plugin](../../sdks/capacitor/file-opener.md) or a share sheet.

## What Capacitor scoped storage means for `@capacitor/filesystem`

Most of the plugin is unaffected. Scoped storage restricts shared storage, not the app sandbox, so every app-specific directory behaves exactly as it did before Android 10 and needs no permission on any version.

| 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. On Android 11 and newer your app only sees files and folders it created itself. |
| `Directory.ExternalStorage` | Gone. Documented as not accessible on Android 11 or newer. |

The Capacitor File Manager plugin keeps the same `Directory` member names and values, with two deliberate differences. `Directory.ExternalStorage` does not exist at all, because there is nothing left for it to point at. And [`Directory.Documents`](../../sdks/capacitor/file-manager.md#directory) maps to the app-specific documents directory (`getExternalFilesDir(DIRECTORY_DOCUMENTS)`) rather than the public Documents folder, so it needs no storage permission and behaves the same on every Android version.

## 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 read and write 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](https://developer.android.com/training/data-storage/manage-all-files){:target="_blank"} under a dedicated policy since May 2021, Android Studio raises a lint warning when you add it, 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 and restore tools, anti-virus apps, and similar categories where browsing arbitrary files is the product. An expense app that wants to drop receipts into a folder is not in that group, and reviewers treat a document-management use case as one the Storage Access Framework already covers.

One limit survives the permission. Even with all-files access granted, the Storage Access Framework itself does not widen, and you can only reach a file or directory through it if you could reach it without the permission.

## How the Storage Access Framework replaces shared storage 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`](https://developer.android.com/training/data-storage/shared/documents-files){:target="_blank"} intent, available since Android 5.0 (API level 21), the system picker opens, and the user selects a directory. What comes back is a tree URI that covers that directory and everything beneath it, and nothing else.

By default that grant expires when the device restarts. Calling `takePersistableUriPermission()` with the read and write flags makes it survive reboots and app restarts, which is what turns a one-off picker result into a folder your app can keep using. Two limits apply. The grant is lost if the document behind it is moved or deleted, in which case the user has to pick again. And on Android 11 and higher, the picker refuses to grant the internal storage root, the roots of reliable SD card volumes, and the `Download` directory.

Nothing in this flow requires a permission declaration in your manifest. The user's choice in the system picker is the permission.

## Persistent folder access with the Capacitor File Manager plugin

The Capacitor File Manager plugin implements the whole framework flow behind two methods, so you never touch a tree URI or a persistable permission flag yourself. Start by letting the user pick a directory with [`pickDirectory()`](../../sdks/capacitor/file-picker.md#pickdirectory) from the [Capacitor File Picker plugin](../../sdks/capacitor/file-picker.md), then hand the result to [`persistDirectoryAccess(...)`](../../sdks/capacitor/file-manager.md#persistdirectoryaccess):

```ts
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 base64-encoded security-scoped bookmark, and it requires version `8.1.0` or later of the File Picker plugin. On Android it is ignored, because the persisted tree URI already carries the grant.

Do not store the returned URI in your own preferences. It can change between app launches, so read the current list on startup with [`getPersistedDirectories()`](../../sdks/capacitor/file-manager.md#getpersisteddirectories), which also refreshes stale entries and drops directories whose document no longer exists:

```ts
import { FileManager } from '@capawesome-team/capacitor-file-manager';

const restoreExportFolder = async () => {
  const { directories } = await FileManager.getPersistedDirectories();
  return directories[0] ?? null;
};
```

Once a directory is persisted, its URI is accepted by every method of the plugin. Build the URI of a file inside it with [`getUri(...)`](../../sdks/capacitor/file-manager.md#geturi) by passing `parentUri` instead of `directory`, then write, copy, move or read as usual:

```ts
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;
};
```

One Android caveat comes from the document provider layer rather than the plugin. Constructing the URI of an entry that does not exist yet inside a persisted directory only works for path-structured providers such as the local device storage, so a cloud provider mounted into the picker may not support it. [`copyFile(...)`](../../sdks/capacitor/file-manager.md#copyfile) and [`moveFile(...)`](../../sdks/capacitor/file-manager.md#movefile) also return the URI of the created file, which can differ from the one you requested if the provider renamed the file to avoid a collision, so use the returned value rather than the one you built.

When the user revokes a folder in your settings screen, call [`releaseDirectoryAccess(...)`](../../sdks/capacitor/file-manager.md#releasedirectoryaccess). If you only want to empty a folder, use [`clearDirectory(...)`](../../sdks/capacitor/file-manager.md#cleardirectory) instead of deleting and recreating it, since deleting the directory also destroys the persisted grant.

### Handing a persisted file to other plugins

A persisted directory URI is a `content://` URI on Android and a security-scoped `file://` URI on iOS, and plugins that expect a plain file path will not accept either. The bridge is a copy into the sandbox with [`copyFile(...)`](../../sdks/capacitor/file-manager.md#copyfile), typically into `Directory.Cache`, where every file-related Capawesome plugin can work with it before you copy the result back:

```ts
import { Directory, FileManager } from '@capawesome-team/capacitor-file-manager';

const stageForProcessing = async (sourceUri: string) => {
  const { uri: cacheUri } = await FileManager.getUri({
    path: 'staged.pdf',
    directory: Directory.Cache,
  });
  await FileManager.copyFile({ uri: sourceUri, toUri: cacheUri });
  return cacheUri;
};
```

For reading a large file out of a persisted folder into your web code, use [`readFileAsBlob(...)`](../../sdks/capacitor/file-manager.md#readfileasblob) rather than a copy. It streams the file into a `Blob` without the base64 encoding that makes big reads run out of memory.

## 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 path.

| 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` or `Directory.Temporary` |
| Large app-owned files on the shared volume | `Directory.External` |
| App-owned documents with no permission prompt | `Directory.Documents` (File Manager plugin) |
| A user-visible folder your app keeps using across launches | [`pickDirectory()`](../../sdks/capacitor/file-picker.md#pickdirectory) plus [`persistDirectoryAccess(...)`](../../sdks/capacitor/file-manager.md#persistdirectoryaccess) |
| A single file the user selects once | [`pickFiles(...)`](../../sdks/capacitor/file-picker.md#pickfiles) from the Capacitor File Picker plugin |
| Photos, videos or audio that appear in the gallery | MediaStore collections (platform API) |
| A download the user finds in `Download/` | The `MediaStore.Downloads` collection |
| Browsing arbitrary files across the whole device | `MANAGE_EXTERNAL_STORAGE`, only if that is your app's core function |

Checking free space before a large export belongs in the same decision. [`getDeviceStorageInfo()`](../../sdks/capacitor/file-manager.md#getdevicestorageinfo) returns total, free and allocatable bytes, so you can fail early instead of halfway through a copy.

## The Capacitor issues that are still open

Two issues in the `ionic-team/capacitor-filesystem` repository have tracked this gap for years without a resolution:

- [#28, "feat(android): update the filesystem plugin to accommodate scoped storage and permission changes since Android 10"](https://github.com/ionic-team/capacitor-filesystem/issues/28){:target="_blank"}, opened on 30 December 2020 and still open more than five years later.
- [#37, "Clarifications on @capacitor/filesystem Directory enum"](https://github.com/ionic-team/capacitor-filesystem/issues/37){:target="_blank"}, opened on 31 October 2023, asking which `Directory` member to use for which purpose now that half the enum behaves differently per Android version.

Until one of them lands, `@capacitor/filesystem` covers the sandbox and stops at the boundary of shared storage. The Capacitor File Manager plugin covers the other side of that boundary through persisted directories.

## FAQ

### Do I still need `WRITE_EXTERNAL_STORAGE` in a Capacitor app?

No, not if your app targets Android 11 or higher, where the permission provides no additional access. The Capacitor File Manager plugin declares no storage permissions at all: app sandbox directories never needed one, and user-visible folders are granted through the system file picker instead.

### Can the user grant my app the Downloads folder?

Not the `Download` directory itself. Android 11 and higher exclude it from `ACTION_OPEN_DOCUMENT_TREE`, alongside the internal storage root and the roots of reliable SD card volumes. Ask the user to pick a different folder, or write into the `MediaStore.Downloads` collection for files your own app creates.

### Does scoped storage affect iOS?

No, iOS has always sandboxed apps, so there is no equivalent regression to fix. It reaches folders outside the sandbox through security-scoped bookmarks, which is why [`persistDirectoryAccess(...)`](../../sdks/capacitor/file-manager.md#persistdirectoryaccess) takes a `bookmark` parameter next to the URI. The same two lines of app code cover both platforms.

### Will a persisted folder URI stay valid forever?

No. Android drops the grant when the document behind it is moved or deleted, and the user can revoke access in the system settings at any time. Calling [`getPersistedDirectories()`](../../sdks/capacitor/file-manager.md#getpersisteddirectories) on app start returns only the directories that are still usable, so treat an empty result as a signal to send the user back through the picker.

### What happens on the web?

Persisted directories and checksums are not available there. The web implementation of the plugin stores files in the Origin Private File System with real directories and random access, which is a different model than a user-granted folder on Android. Plan for the web platform as sandbox-only.

## Stay Ahead of Android Storage Changes

Storage rules change with almost every Android release, and the deprecations tend to surface as runtime failures rather than build errors. Our newsletter covers the platform changes that affect Capacitor apps, plus new plugins and releases.

[Subscribe to the Capawesome Newsletter](https://capawesome.io/newsletter/){ .md-button .md-button--primary }

## Conclusion

If your app writes anywhere outside its own sandbox on Android, stop looking for a directory constant that still works and move that code to a folder the user grants you. Audit your project for `Directory.ExternalStorage` and for `Directory.Documents` on Android, replace those call sites with 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 it once.

For the wider picture on reading, writing, sharing and downloading files without running into out-of-memory errors, our [Capacitor File Handling guide](./capacitor-file-handling-guide.md) is the place to start. Questions about a specific storage case? Join the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"} and ask, or subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"} to stay on top of platform changes.
