Mobile Computing · Mobile Storage
App Container Layout and Which Files Belong in a Backup
How the sandbox directory tree separates durable user data from regenerable caches, what the platform backup excludes by rule, and why the restore path is the one that breaks.
An installed application does not own a filesystem. It owns a subtree of one, with rules about what the system may delete, what it may copy, and what it will copy back on a new device. Those rules are not documentation for the storage API; they are the specification the application must satisfy, and a file placed in the wrong subtree silently changes its lifetime from “until the user deletes the app” to “until the system needs space”.
The two dominant platforms express the same idea with different directory names, and the differences matter because a design that treats them as interchangeable usually ends up with user data in a cache.
Three lifetimes, not one
Every file in a container falls into one of three classes, and the class is determined by location rather than by content.
Files the user would notice missing are durable: they are included in the platform backup, they survive an app update, and they are removed only when the app is uninstalled. Files that can be regenerated from a server or from the durable data are caches: the system may delete them at any time, including while the app is not running, and the app must tolerate finding them gone. Files that are temporary exist to be renamed or deleted within a single session and have no guarantee beyond that.
The durable class is the smallest, because the platform charges for it. On iOS the container has a Documents directory whose contents are backed up, a Library directory for application support data and preferences, and a tmp directory that is purged. Within Library, a Caches subdirectory is the designated place for regenerable data. On Android the equivalent split is internal storage, where getFilesDir() gives a directory that is backed up by default and getCacheDir() gives one the system may clear, plus external storage areas with their own visibility rules.
The mapping is not one-to-one, and the differences are instructive. Android’s cache directory is scoped to the app and cleared under storage pressure; iOS’s Library/Caches is likewise excluded from backup but is not cleared on demand in the same way: the system may purge it when the device is low on space, and the app is expected to keep working. Neither platform guarantees that a cache file survives to the next launch.
The right classification test is a question about consequences: if this file disappears while the app is not running, does the app lose information the user provided? If yes, it is durable. If the app can rebuild it by re-fetching or re-deriving, it is a cache. Files that are durable but large, downloaded media the user chose to keep offline, are the awkward case, and both platforms provide a way to mark them as excluded from backup while leaving them in a durable directory.
Marking a file excluded
iOS attaches attributes to individual URLs rather than to directories. The NSURLIsExcludedFromBackupKey resource key, applied to a file or directory URL, removes it from the backup set; the attribute is inherited by contents when set on a directory, and it is stored with the file so it survives a copy that preserves attributes.
Android’s mechanism is declarative and coarser: a backup rules file referenced from the manifest lists which directories and files are included or excluded, expressed as path patterns. The exclusion applies to the whole app’s backup, not to individual files at runtime, so a file whose backup-worthiness changes at runtime cannot be handled by the rules alone; it has to be moved between directories.
The asymmetry has a design consequence. On iOS an application can decide per file, at the moment it creates it, whether the file is backed up. On Android the decision is a property of the path, so the directory layout must encode the decision. A portable design therefore puts the decision in the path even on iOS, which makes the two platforms behave the same way and makes the Android rules file straightforward.
What the backup actually copies
The backup is not a filesystem snapshot. On iOS the device backup copies files that are not excluded, and the system’s own rules also skip certain locations regardless of attributes. On Android the auto-backup mechanism copies the app’s data with a size limit per app; 25 MB is the documented quota for the standard auto-backup, and an app that exceeds it is not backed up at all, or is backed up partially depending on how the rules are written.
The size limit is the constraint that most often changes a design. A local database with a year of message history is easily larger than the quota, and the platform’s response is not to back up a truncated version but to skip the app. The fix is to exclude the bulk store from backup and keep in the backed-up area only what is needed to reconstruct: identifiers, sync cursors, and a record of what the user has marked as important. That is the same reasoning as change logs and tombstones, applied to the backup boundary rather than to a sync boundary.
The backup is also incremental at the file level, not the block level. A database file that changes on every launch is a new file for backup purposes each time, so a large database in a backed-up directory consumes the quota repeatedly. Splitting the durable data into a small, rarely changing part and a large, frequently changing part that is excluded keeps the incremental cost proportional to what actually changed.
The restore path
Restoring is where the classification gets tested, because the restored container is not the one that was backed up.
A restored database may be older than the app version that opens it, so the schema migration path must handle a jump across several versions rather than one step. This is the case that on-device schema migration has to accommodate, and it is the reason migrations should be expressed as a sequence of forward steps from any earlier version rather than as pairwise upgrades.
A restored container may also arrive with files that reference absolute paths. An app that stores the absolute path of a media file in its database will find that the path is different on the new device, because the container identifier changes between installations. Storing a relative path, or a stable identifier resolved at read time, avoids the problem entirely.
Restored caches do not exist, by definition, and the app must not assume they do. A first launch after restore is a first launch in every respect except that the durable data is present; any code path that depends on a cache being populated will fail. The failure is usually a performance problem rather than a correctness one, which makes it easy to ship.
A restored container can also contain files the current app version no longer understands. A cache format that changed between versions, a journal from a database that has since been migrated, a temporary file from an interrupted save: all of these arrive intact because the backup copied them. The app’s startup must be able to identify and discard them, which means every durable file needs a version marker in its own content, not just in a database schema.
The container is not private in every sense
The sandbox prevents other applications from reading the container; it does not prevent the device owner from doing so, and on a device with a compromised boot chain it does not prevent anything. The platform’s data protection classes, which tie file encryption keys to the device passcode and to the lock state, are the mechanism that closes that gap, and they are a per-file property in the same way the backup attribute is.
The practical rule is that the container’s directory structure carries three independent per-file decisions: whether the file is backed up, whether it is encrypted at rest under a protection class, and whether it is excluded from the system’s search index. None of these are implied by the others, and all three are cheap to set at creation and expensive to retrofit.
A file that is created in the wrong place and later moved has usually already been backed up once, and the backup system may retain the old copy until the next full backup. Excluding it retroactively is possible but leaves the earlier copy in the backup set until the set is rebuilt. Creating files in the right directory from the first launch is the only reliable approach.