diff --git a/.github/workflows/test-cpp.yml b/.github/workflows/test-cpp.yml
new file mode 100644
index 00000000..fded1fce
--- /dev/null
+++ b/.github/workflows/test-cpp.yml
@@ -0,0 +1,54 @@
+name: Test C++
+
+on:
+ push:
+ branches:
+ - main
+ paths:
+ - ".github/workflows/test-cpp.yml"
+ - "packages/react-native-nitro-sqlite/cpp/databaseMigration.*"
+ - "packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp"
+ - "packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.*"
+ - "packages/react-native-nitro-sqlite/tests/cpp/**"
+ pull_request:
+ paths:
+ - ".github/workflows/test-cpp.yml"
+ - "packages/react-native-nitro-sqlite/cpp/databaseMigration.*"
+ - "packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp"
+ - "packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.*"
+ - "packages/react-native-nitro-sqlite/tests/cpp/**"
+
+jobs:
+ test:
+ name: Database migration tests
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: Build bundled SQLite
+ run: |
+ clang \
+ -std=c11 \
+ -DSQLITE_THREADSAFE=2 \
+ -c packages/react-native-nitro-sqlite/cpp/sqlite/sqlite3.c \
+ -o /tmp/sqlite3.o
+
+ - name: Build migration tests
+ run: |
+ clang++ \
+ -std=c++20 \
+ -Wall \
+ -Wextra \
+ -Werror \
+ -Ipackages/react-native-nitro-sqlite/cpp \
+ -Ipackages/react-native-nitro-sqlite/cpp/sqlite \
+ packages/react-native-nitro-sqlite/cpp/databaseMigration.cpp \
+ packages/react-native-nitro-sqlite/tests/cpp/databaseMigration.test.cpp \
+ /tmp/sqlite3.o \
+ -ldl \
+ -lm \
+ -pthread \
+ -o /tmp/databaseMigrationTests
+
+ - name: Run migration tests
+ run: /tmp/databaseMigrationTests
diff --git a/README.md b/README.md
index b5c7e208..51528ae4 100644
--- a/README.md
+++ b/README.md
@@ -373,6 +373,23 @@ nitroSqliteFlags="-DSQLITE_ENABLE_FTS5=1"
To put the database in an app group (e.g. for extensions), set `RNNitroSQLite_AppGroup` in your `Info.plist` to the app group ID and add the App Groups capability in Xcode.
+## Database location (iOS)
+
+By default, databases are stored in the app's **Documents** directory. If your app enables file sharing (`UIFileSharingEnabled` + `LSSupportsOpeningDocumentsInPlace`), that directory — including your raw database and its `-wal`/`-shm` journal files — becomes visible to users in the Files app, where they can be shared, modified, or deleted from outside your app.
+
+To store databases in `Library/Application Support` instead (persistent, backed up, and never user-visible), set `RNNitroSQLite_DatabaseLocation` in your `Info.plist`:
+
+```xml
+RNNitroSQLite_DatabaseLocation
+ApplicationSupport
+```
+
+Supported values are `Documents` (the default) and `ApplicationSupport`.
+
+Databases created while the app was still using the Documents directory are automatically moved to `Library/Application Support` the first time they are opened or attached after enabling this option, so existing users keep their data. Deleting a database also removes any copy left in Documents by an interrupted migration. If you later remove the option, databases already moved to `Library/Application Support` are **not** moved back.
+
+This option has no effect when `RNNitroSQLite_AppGroup` is set, since app group databases live in the shared container.
+
---
# Exports
diff --git a/packages/react-native-nitro-sqlite/cpp/databaseMigration.cpp b/packages/react-native-nitro-sqlite/cpp/databaseMigration.cpp
new file mode 100644
index 00000000..1919d8d1
--- /dev/null
+++ b/packages/react-native-nitro-sqlite/cpp/databaseMigration.cpp
@@ -0,0 +1,123 @@
+#include "databaseMigration.hpp"
+#include "logs.hpp"
+#include
+#include
+
+namespace margelo::nitro::rnnitrosqlite {
+
+namespace fs = std::filesystem;
+
+namespace {
+
+ constexpr std::size_t kDatabaseFileCount = 4;
+ using DatabaseFiles = std::array;
+
+ DatabaseFiles getDatabaseFiles(const std::string& dbName);
+ bool copyDatabaseFiles(const DatabaseFiles& files, const fs::path& fromDirectory, const fs::path& toDirectory);
+ void removeAuxiliaryDatabaseFiles(const DatabaseFiles& files, const fs::path& directory);
+
+} // namespace
+
+fs::path migrateDatabase(const std::string& dbName, const fs::path& fromDirectory, const fs::path& toDirectory) {
+ const auto files = getDatabaseFiles(dbName);
+ std::error_code ec;
+ const bool sourceExists = fs::exists(fromDirectory / dbName, ec);
+
+ if (ec) {
+ LOGW("Failed to inspect database %s in its old location: %s", dbName.c_str(), ec.message().c_str());
+ return fromDirectory;
+ }
+
+ if (!sourceExists) {
+ // A completed migration may have been interrupted after deleting the database but before
+ // deleting its journals. The destination is already authoritative in that state.
+ removeAuxiliaryDatabaseFiles(files, fromDirectory);
+ return toDirectory;
+ }
+
+ // A database in the old directory is the live copy. Clear every database generation file at
+ // the destination before copying so SQLite never pairs the source with a stale journal.
+ if (!removeDatabaseFiles(dbName, toDirectory)) {
+ return fromDirectory;
+ }
+
+ fs::create_directories(toDirectory, ec);
+ if (ec) {
+ LOGW("Failed to create database migration directory %s: %s", toDirectory.string().c_str(), ec.message().c_str());
+ return fromDirectory;
+ }
+
+ if (!copyDatabaseFiles(files, fromDirectory, toDirectory)) {
+ return fromDirectory;
+ }
+
+ // Delete the database first. If this fails, every source journal must remain beside it so the
+ // caller can safely keep using the old location. Leftover journals after a successful database
+ // deletion are harmless and are removed on the next migration attempt.
+ if (!fs::remove(fromDirectory / dbName, ec) || ec) {
+ LOGW("Failed to remove migrated database %s from its old location: %s", dbName.c_str(), ec.message().c_str());
+ return fromDirectory;
+ }
+
+ removeAuxiliaryDatabaseFiles(files, fromDirectory);
+ return toDirectory;
+}
+
+bool removeDatabaseFiles(const std::string& dbName, const fs::path& directory) {
+ const auto files = getDatabaseFiles(dbName);
+
+ for (const auto& file : files) {
+ std::error_code ec;
+ fs::remove(directory / file, ec);
+ if (ec) {
+ LOGW("Failed to remove database file %s: %s", file.c_str(), ec.message().c_str());
+ return false;
+ }
+ }
+
+ return true;
+}
+
+namespace {
+
+ DatabaseFiles getDatabaseFiles(const std::string& dbName) {
+ return {dbName, dbName + "-journal", dbName + "-wal", dbName + "-shm"};
+ }
+
+ bool copyDatabaseFiles(const DatabaseFiles& files, const fs::path& fromDirectory, const fs::path& toDirectory) {
+ for (const auto& file : files) {
+ std::error_code ec;
+ const bool sourceExists = fs::exists(fromDirectory / file, ec);
+
+ if (ec) {
+ LOGW("Failed to inspect database file %s: %s", file.c_str(), ec.message().c_str());
+ return false;
+ }
+
+ if (!sourceExists) {
+ continue;
+ }
+
+ if (!fs::copy_file(fromDirectory / file, toDirectory / file, ec) || ec) {
+ LOGW("Failed to migrate database file %s: %s", file.c_str(), ec.message().c_str());
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ void removeAuxiliaryDatabaseFiles(const DatabaseFiles& files, const fs::path& directory) {
+ for (std::size_t index = 1; index < files.size(); index++) {
+ const auto& file = files[index];
+ std::error_code ec;
+ fs::remove(directory / file, ec);
+ if (ec) {
+ LOGW("Failed to remove database file %s: %s", file.c_str(), ec.message().c_str());
+ }
+ }
+ }
+
+} // namespace
+
+} // namespace margelo::nitro::rnnitrosqlite
diff --git a/packages/react-native-nitro-sqlite/cpp/databaseMigration.hpp b/packages/react-native-nitro-sqlite/cpp/databaseMigration.hpp
new file mode 100644
index 00000000..a9668ac6
--- /dev/null
+++ b/packages/react-native-nitro-sqlite/cpp/databaseMigration.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include
+#include
+
+namespace margelo::nitro::rnnitrosqlite {
+
+std::filesystem::path migrateDatabase(const std::string& dbName, const std::filesystem::path& fromDirectory,
+ const std::filesystem::path& toDirectory);
+
+bool removeDatabaseFiles(const std::string& dbName, const std::filesystem::path& directory);
+
+} // namespace margelo::nitro::rnnitrosqlite
diff --git a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp
index 74983e0a..94c576dd 100644
--- a/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp
+++ b/packages/react-native-nitro-sqlite/cpp/hybridObjects/HybridNitroSQLite.cpp
@@ -1,12 +1,14 @@
#include "HybridNitroSQLite.hpp"
#include "HybridNitroSQLiteQueryResult.hpp"
#include "NitroSQLiteException.hpp"
+#include "databaseMigration.hpp"
#include "importSqlFile.hpp"
#include "logs.hpp"
#include "macros.hpp"
#include "operations.hpp"
#include "sqliteExecuteBatch.hpp"
#include
+#include
#include
#include