SQLite in a React Native app is one file on the phone, and the phone is already backing it up.
SQLite in a React Native app is a database that lives on the device as a single file inside your app's own sandbox. In an Expo project the library is expo-sqlite, and Expo's reference says the database "is persisted across restarts of your app". No server, no account, no network call for a read. That is the appeal, and it is also where the surprises hide. Expo documents the API in great detail and says nothing at all about whether that file ends up in a cloud backup. Both operating systems have already answered that question for you, and the answer is mostly yes. If you were handed this project rather than typed it, start there and not with the SQL. Some background on what React Native is helps, but none of what follows is really a React Native problem.
What follows is where the file actually sits, who copies it, what breaks when you change a table, and when one phone stops being enough. Every quote below was read against first-party documentation on 17 October 2026, against the expo-sqlite version the docs recommend today, 58.0.13. Then the one number that decides whether Android keeps backing your database up at all.
Start hereThe short version
One file, no server, and two operating systems with opinions about it.
A SQLite database in an Expo app is a file your code opens by name. You call openDatabaseAsync with a database name, Expo puts the file in a default directory, and from then on every query is a local function call. Nothing fails because the train went into a tunnel. Nothing costs money per read. For a notes app, a shopping list, a workout log or an offline catalogue, this is the correct architecture and not a compromise you make until you can afford better.
The parts that cost people time are not the SQL. They are that the file sits in a sandbox you cannot reach, that both operating systems treat it as user data and copy it to the cloud, and that every phone out there already has the schema you shipped last time. Those three facts are what it means to keep data on a device you do not own, and they are the whole subject of this page.
Where does the file live, and can you look inside it?
Expo publishes a constant called SQLite.defaultDatabaseDirectory and describes it in one line: "The default directory for SQLite databases." It prints no path, for any platform. Read the reference looking for one and you will not find it, which is worth saying plainly instead of repeating a path you saw somewhere else. The functions that take a location, openDatabaseAsync and deleteDatabaseAsync, both default to that same directory.
One sentence gives the Apple answer away, and it does it by describing an exception. Expo's note reads: "On Apple TV, the underlying database file is in the caches directory and not the application documents directory, per Apple platform guidelines." tvOS is the odd one out. Everywhere else on Apple hardware the file is in the application documents directory, which is the directory the system treats as user data worth keeping. On Android, Expo gives no equivalent hint, and the two plausible directories behave the same way for the purposes of the next section, so the gap matters less than it looks.
If you want the real path rather than an inference, the open database object has a read only databasePath property. Log it once on a device in development and you have the answer for that platform. Expo does not document its format, so printing it beats assuming it.
You can also simply look at the data, which is the first thing to do with a project you did not write. Expo says expo-sqlite includes a built-in DevTools inspector plugin that is "automatically enabled in development and requires no extra setup". Press Shift and M in the Expo CLI terminal to open the dev tools menu, then choose Open expo-sqlite. It browses tables, views and edits rows, runs SQL queries and exports databases from your browser. Fifteen minutes in there tells you more about what your app stores than any amount of reading the code.
Two settings move the file somewhere else, and both are deliberate. openDatabaseAsync takes a directory parameter, which Expo says is not supported on web. And on iOS you can put the database in an App Group shared container, using Paths.appleSharedContainers from expo-file-system, so a widget or a share extension can read the same rows. If nobody set either of those, the default directory is where your database is.
Is the database backed up, and who decided that?
Expo does not answer this, and the careful version of that sentence matters. On 17 October 2026 we read the expo-sqlite reference and the expo-file-system reference end to end. Neither page mentions iCloud, device backup, or excluding a file from a backup. The word backup appears twice on the SQLite page, both times for backupDatabaseAsync and backupDatabaseSync, which copy one open SQLite database into another. That is a SQLite operation. It has nothing to do with the user's phone backup, and reading the method name as reassurance is the mistake this section exists to stop.
So the operating system decides, and both have already decided. Apple's page Optimizing Your App's Data for iCloud Backup opens with it: when iCloud Backup is in an enabled state, it periodically creates a backup of the user's device, "including your app's data". Apple then names exactly two directories the backup skips, /tmp and /Library/Caches, because the system purges them anyway, and warns that for anything else "iCloud Backup may include that data". By that list, a database in the application documents directory is in the backup. Apple does not name expo-sqlite, or any directory by the name Expo uses, so the chain runs through Expo's own note about Apple TV above. Say so when you repeat it.
Apple does offer a way out, and is unusually blunt about how strong it is. You can set a file or directory's isExcludedFromBackup resource value. Apple then says that value "exists only to provide guidance to the system about which files and directories it can exclude; it's not a mechanism to guarantee those items never appear in a backup or on a restored device". Apple also warns that some file operations reset resource values, so you have to set it again every time you save the file. Read both sentences twice before you promise anyone that something stays on one device.
Android backs it up too, and puts a number on it. Auto Backup applies to apps that target and run on Android 6.0, API level 23, or higher. Its default set includes "Files in the directory returned by getDatabasePath(String), which also includes files created with the SQLiteOpenHelper class", plus files in internal storage, and it excludes the cache, code cache and no-backup directories. The manifest attribute android:allowBackup defaults to true. The data goes to "a private folder in the user's Google Drive account", end to end encrypted on Android 9 and higher using the device PIN, pattern or password. A run happens when the user has backup on, at least 24 hours have passed since the last one, the device is idle and it is on Wi-Fi, and Google says that in practice those conditions "occur roughly every night".
The number is 25 MB. Google writes that "Every app can allocate up to 25 MB of backup data per app user", and that past it "the system calls onQuotaExceeded() and doesn't back up data to the cloud". A local database that grows with use will cross that line quietly. Nobody gets an alert, no build fails, and the user finds out on a new phone. If your app stores photos, attachments or a long history in SQLite, that figure is the most useful thing on this page.
If you decide the database should not travel, each platform has its own switch and they are not the same shape. On Android 12 and higher, for an app targeting API level 31 or higher, you point android:dataExtractionRules at an XML file with cloud-backup, device-transfer and cross-platform-transfer sections. Below that it is android:fullBackupContent with include and exclude elements, where the valid domains include database, file and sharedpref, exclude takes precedence over include, and Google warns that once you specify an include "the system no longer includes any files by default". On iOS it is the resource value above, with Apple's own caveat attached to it.
Android Developers, back up user data with Auto Backup, read 17 October 2026
Check yours
Is that directory in the backup?
Pick where the file sits. The verdict is the documented default, not a guess, and it changes if somebody on your side wrote a rule.
iOS, iPadOS, macOS
In the cloud backup by default
This is where expo-sqlite puts the file on Apple platforms other than tvOS. Apple names only /tmp and /Library/Caches as excluded by default, and says that otherwise iCloud Backup may include the data. So the database travels with the user.
Defaults from Apple and Google documentation read on 17 October 2026. Check your own manifest and your own file paths before you rely on either column.
What breaks when you change a table?
The new install is the easy case. Your setup code runs CREATE TABLE, the tables match the queries, the app works. The existing install is the one that bites. That phone already has the tables you shipped last time, with somebody's real rows in them, and your new query asks for a column that is not there. On a server you would run a migration and watch it. Here it runs on a stranger's phone, once, and you never see it.
Expo documents a pattern rather than shipping a framework. SQLiteProvider takes an onInit prop, described as "A custom initialization handler to run before rendering the children" that you can use "to run database migrations or other setup tasks". The example in the reference reads PRAGMA user_version, compares it against a DATABASE_VERSION constant in your own code, runs the statements for each step it is behind, then writes the new number back with PRAGMA user_version. It is a version counter and a staircase of if blocks. That is all SQLite gives you, and it is enough, as long as nobody ever edits a step that already shipped.
There is no migration tooling inside the library. Expo says expo-sqlite "is designed to be a solid SQLite foundation" and points at third-party integrations instead, naming Drizzle ORM, which it says "has a CLI companion called drizzle-kit for generating SQL migrations", and Knex.js. If your project already has Drizzle in it, somebody made that choice on your behalf and the migration files are sitting in the repo waiting to be read.
Two details about timing are worth holding on to. onInit runs before the children render, so your migration is on the cold start path and a slow one is a slow launch, out of the same budget as everything else in app startup time. And the onError prop defaults to rethrowing the error. A migration that throws, throws at launch, on every existing install at once, and you cannot reach that file to fix it. Ship a schema change the way you would ship a database change to a server you have no login for.
Do you need a server, and when does one phone stop being enough?
No server, and for a lot of apps that is the honest answer rather than a stage you grow out of. A local database has no hosting line, no scaling plan and no invoice that grows with signups, which is a large slice of what an app costs to maintain when the app is mostly one person's own data. The recurring bill you cannot avoid is the store side, and that is the same whether your data is local or not, which is the subject of what it costs to publish an app.
One phone stops being enough at predictable moments. The first is a second device for the same person, because nothing syncs. The second is any feature where someone else has to see the data. The third is a lost phone, and this is the one people misread: the OS backup is the user's safety net, not yours. You cannot read it, you cannot restore one record from it, and you cannot prove it ran. If losing the data would be a support problem for you rather than a shame for them, you need your own copy.
Two more facts catch people out. The database is not encrypted by default. Expo supports SQLCipher, "a fork of SQLite that adds encryption and authentication to the database", on Android, iOS and macOS, through a useSQLCipher config plugin property followed by npx expo prebuild, with the password set using PRAGMA key right after you open the database. Expo notes SQLCipher is not supported in Expo Go, so it needs a new build rather than a reload. Decide it before you have users, because turning it on later means moving existing rows out of a plain file.
If the project also targets web, that is a different engine under the same API. Expo says that on web expo-sqlite "uses a prebuilt WebAssembly build of SQLite without full-text search (FTS)". The directory parameter is not supported there, nor is withExclusiveTransactionAsync, and a deployed web build needs Cross-Origin-Embedder-Policy and Cross-Origin-Opener-Policy headers before the thing will load at all. A feature that works on the phone can be missing on the web build for reasons that have nothing to do with your code.
You may also have a SQLite database you never asked for. expo-sqlite ships a key-value Storage API as a drop-in replacement for AsyncStorage, backed by SQLite, so an app that only saves preferences may still be keeping them in a SQLite file under every rule above. And one line on security is worth repeating in full, because it is the only vulnerability class this library can hand you: Expo says you "must escape all user input passed to SQLite", and that prepared statements are the defence, because they separate the query from its parameters.
The same local file, two different backup regimes
| The question | iOS, iCloud Backup | Android, Auto Backup | Same answer on both |
|---|---|---|---|
| Your local database is in the cloud backup by default | Yes, outside /tmp and /Library/Caches | Yes, from the databases and files directories | Yes |
| Where the copy goes | The user's iCloud backup of the device | A private folder in the user's Google Drive | No |
| A documented size cap | No figure given on Apple's page | 25 MB per app user, then onQuotaExceeded | No |
| A documented way to opt the file out | isExcludedFromBackup, described as guidance only | allowBackup, dataExtractionRules, fullBackupContent | No |
| Expo's own documentation covers the behaviour | No | No | Yes |
Where this lands if a builder generated your app
Newly is an AI app builder. You describe an app in plain English and it writes a real React Native and Expo project that you own, with two-way GitHub sync, so the openDatabaseAsync call and the migration function are files you can open and read rather than a black box. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK. It does not submit to the App Store for you, because Apple requires the owner of the content to submit. It is $25 a month and there is no free tier.
This page exists because a generated app usually starts in exactly this shape. Data on the phone, no accounts, no server, one SQLite file. That is a sensible default and it ships. What a generator cannot decide for you is everything above. Whether a user's rows should ride along in their cloud backup. Whether the file needs a password. What version two of your schema does to the person who installed version one last month. Those are product decisions with documented consequences, and they stay yours no matter who typed the code.
The practical move for somebody who inherited the project is short. Open the inspector and look at the tables. Find the onInit function and read what it does when the version is behind. Log databasePath once so you know which directory you are arguing about. Then decide, on purpose, whether this data belongs in the user's cloud backup. That is an afternoon, and it is the difference between owning the codebase and merely having it.
Questions people ask about SQLite in a React Native app
No. The database is a file on the phone and your queries are local calls. There is no account to create, no network for a read to depend on and no hosting bill. In an Expo project the library is expo-sqlite, and Expo's reference says the database is persisted across restarts of your app. A server becomes necessary when the same person needs the data on two devices, when other people need to see it, or when you want a copy you can restore yourself rather than one the operating system holds on the user's behalf.
Describe the app, then decide where its data sleeps
Start local, because most apps should. Then make the backup decision, the encryption decision and the schema version a choice you made rather than a default you inherited.
Start building