Skip to content

feat(ios): add option to store databases in Library/Application Support - #323

Open
NicolasBonet wants to merge 1 commit into
margelo:mainfrom
NicolasBonet:feat/ios-application-support-location
Open

feat(ios): add option to store databases in Library/Application Support#323
NicolasBonet wants to merge 1 commit into
margelo:mainfrom
NicolasBonet:feat/ios-application-support-location

Conversation

@NicolasBonet

Copy link
Copy Markdown

Motivation

Fixes #289.

On iOS (without an App Group), databases are stored in the app's Documents directory. When an app enables file sharing (UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace), Documents becomes visible to users in the Files app — including the raw SQLite database and its -wal/-shm journals, which can then be shared, modified, or deleted from outside the app. Apple's File System Programming Guide recommends Library/Application Support for app-internal data files: it is persistent, backed up, and never user-visible.

We hit this in production at Expensify and have been shipping this behavior as a patch-package patch since Expensify/App#96531; this PR upstreams it in a generalized, opt-in form.

What this does

Adds a new Info.plist key, following the existing RNNitroSQLite_AppGroup convention:

<key>RNNitroSQLite_DatabaseLocation</key>
<string>ApplicationSupport</string>
  • Supported values: Documents (default, current behavior) and ApplicationSupport. Making this opt-in avoids a breaking change for apps that rely on Files-app access to their databases — though I'd be happy to flip the default in a future major if you prefer, since the migration makes that safe data-wise.
  • When set to ApplicationSupport, OnLoad.mm points docPath at Library/Application Support (creating it if needed) and records the old Documents path in a new HybridNitroSQLite::migrationDocPath.
  • Databases created by older app versions are migrated out of Documents lazily, in HybridNitroSQLite::open(), where the database name is known — so the migration is fully generic. It also honors the location option (migrates Documents/<location>Application Support/<location>).
  • No effect on Android or when RNNitroSQLite_AppGroup is set (app group databases live in the shared container). migrationDocPath stays empty in those cases, making the open() change a no-op.

Migration safety

Committed-but-uncheckpointed writes live in the -wal file, and SQLite only replays a -wal that sits next to its database, so the database and its journals must never be separated. The migration therefore:

  1. Removes any stale set at the destination first (e.g. after a downgrade → re-upgrade cycle), so a -wal from one database generation is never replayed into a database from another.
  2. Copies the whole set (db, -wal, -shm) before deleting anything. If any copy fails, the intact originals in Documents keep being used — open() falls back to opening the database from Documents — and the migration retries on the next open.
  3. Deletes the source database first and its journals only once that succeeds: if the database can't be removed, the fallback keeps opening it from Documents, so its -wal must stay next to it.
  4. Sweeps leftover journal files out of Documents on a later open if a previous run was interrupted between deletes.

This exact strategy has been running in the Expensify app in production.

Notes

  • Migration is wired into open() only. attach()/drop() on a never-opened database would still look in the new location; I kept the scope minimal, but happy to extend it to those paths if you'd like.
  • Uses std::filesystem, available on all deployment targets Nitro supports.

Testing

  • Fresh install with the key set → database is created in Library/Application Support.
  • Upgrade path: database created in Documents by a build without the key, then opened by a build with the key → database and journals moved to Library/Application Support, uncheckpointed WAL content preserved.
  • Without the key (or with Documents) → behavior unchanged.

🤖 Generated with Claude Code

The Documents directory becomes user-visible in the Files app when an app
enables file sharing, exposing raw databases and their -wal/-shm journals
to accidental sharing, modification, or deletion.

Setting RNNitroSQLite_DatabaseLocation to "ApplicationSupport" in
Info.plist stores databases in Library/Application Support instead
(persistent, backed up, never user-visible). Databases created by older
app versions are moved out of Documents when they are opened: the
database and its journals are copied as a set before the originals are
deleted, and if anything fails the database keeps being opened from
Documents and the migration retries on the next open.

Fixes margelo#289

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chrispader

Copy link
Copy Markdown
Member

Looking into this! Thanks 🙌🏼

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: databases are stored in the Documents directory, which can be exposed to users via the Files app

2 participants