In an Expo app, how do you encrypt an expo-sqlite database with SQLCipher, and what does that change for builds and connections?
answer
- a native build switch, not JS
- useSQLCipher in the config plugin
- prebuild; no Expo Go
- PRAGMA key first, every connection
- exclusive transaction opens its own
basics
~20 sSet useSQLCipher: true in expo-sqlite's config plugin and rebuild the native app, since Expo Go cannot run it. Then run PRAGMA key as the first statement on every connection, before any other query touches the file.
solid answer
~40 sSQLCipher is a fork of SQLite that encrypts the database file. In expo-sqlite it is a build-time switch: add `["expo-sqlite", { "useSQLCipher": true }]` to the plugins in `app.json`, which writes `expo.sqlite.useSQLCipher` into the Android Gradle properties and iOS Podfile properties, so the native build compiles the vendored SQLCipher instead of SQLite. That needs `npx expo prebuild` or a new development or store build; Expo Go does not support it, and a JavaScript-only update cannot deliver it. At runtime, run `PRAGMA key = '...'` right after opening, before migrations. The key is per connection, and `withExclusiveTransactionAsync` opens a new one, so key `txn` too. Keep the key out of the JavaScript bundle; generate it and hold it in the platform's secure storage.
code
json · 7 lines{
"expo": {
"plugins": [
["expo-sqlite", { "useSQLCipher": true }]
]
}
}go deeper
Recall that SQLCipher is turned on with useSQLCipher in expo-sqlite's config plugin and needs a rebuilt app, not Expo Go.
Explain the runtime side: PRAGMA key as the first statement after opening, before onInit migrations, on each connection including the exclusive transaction's.
Show the key-management plan: generated per install, kept in secure storage, a recovery story if it is lost, and a conversion for existing plaintext files.
Weigh file encryption against its costs, a custom native build and unrecoverable data on key loss, versus keeping sensitive data off the device.
## What SQLCipher adds A plain SQLite file is readable by anyone who can copy it off a device: a backup, a rooted or jailbroken phone, a forensic tool. **SQLCipher** is a fork of SQLite that encrypts the whole database file, pages and all, with a key the app supplies at runtime. The expo-sqlite docs list it as supported on Android, iOS and macOS, not on web. ## Turning it on: a native build switch In expo-sqlite 57, SQLCipher is selected through the library's **config plugin**, not through any JavaScript option: - Add the plugin with `useSQLCipher: true` to `app.json` (or `app.config.ts`). The option defaults to `false`, and it can also be set per platform under `android` or `ios`. - The plugin writes `expo.sqlite.useSQLCipher` into `gradle.properties` on Android and the Podfile properties on iOS. - The native build reads that property and compiles the vendored SQLCipher sources, with SQLCipher's crypto build flags, instead of plain SQLite. Because this changes compiled native code: 1. You must run `npx expo prebuild` (or let EAS Build do it) and produce a **new binary**: a development build for testing, a store build for release. 2. **Expo Go cannot run it**; the docs say SQLCipher is not supported there. Expo Go's native code is fixed, so it cannot switch libraries. 3. A JavaScript-only update cannot switch it on for installed apps; users need the new binary. ## Keying the database at runtime After `openDatabaseAsync`, the first statement on the connection must supply the key: ```ts await db.execAsync(`PRAGMA key = '${key}'`); ``` The docs say to set it right after opening. On an existing encrypted file, any query before it fails because the pages cannot be decrypted; on a brand-new file, queries before it would create the file unencrypted. Either way the key comes first. With `SQLiteProvider`, that means the first lines of `onInit`, before the `user_version` migration ladder reads anything. A key is attached to a **connection**, not to the file. That matters in expo-sqlite in one non-obvious place: `withExclusiveTransactionAsync` creates a **new native connection** for its `txn` object. Keying `db` does not key `txn`, so the task should start with `await txn.execAsync(...)` running the same `PRAGMA key` before its first real query. ## Where the key comes from The encryption is only as strong as the key's storage: - **Never hard-code it** in JavaScript. The bundle ships inside the app, and anyone can extract a string from it. - Generate a random key on first launch and keep it in the platform's secure storage (the Keychain on iOS, the Keystore-backed store on Android), then read it before opening the database. - Decide what happens if the key is lost, for example after a device restore that did not carry secure-storage items. An encrypted journal without its key cannot be recovered, so the app needs a clear path such as a server backup or starting a new database. ## What it does not change | Concern | Effect of SQLCipher | |---|---| | expo-sqlite query API | unchanged: same `runAsync`, `getAllAsync`, transactions | | Migrations with `user_version` | unchanged, after the key is set | | An existing plaintext database | not encrypted by flipping the flag; it needs a one-off conversion with SQLCipher's own tools | | Web builds | not supported | | Build pipeline | now depends on a custom native build | Treat an unencrypted file already on devices as a migration task in its own right: switching to a SQLCipher build and calling `PRAGMA key` on a plaintext file does not encrypt it. ## Verifying it and living with it - **Check that it worked.** Copy the database file off a development build and try to open it with an ordinary SQLite tool; it should refuse to read it as a database. - **Test on a development build**, never Expo Go, and on both platforms, since the property is read separately by the Android and iOS builds. - **Expect some overhead.** Every page read or written is decrypted or encrypted, so measure startup and heavy queries on a low-end Android device. - **Keep the two build flavours apart.** A plaintext file from an old build and an encrypted one from a new build are not interchangeable, so tag bug reports with which one the device has.
- Why can't you enable SQLCipher for installed users with an over-the-air update?`useSQLCipher` changes which native library is compiled into the binary. An over-the-air update only replaces JavaScript and assets, so users need a new build from the store before `PRAGMA key` means anything.
- What happens to a journal database that was already plaintext before the SQLCipher build shipped?It stays plaintext. The flag changes the library, not existing files, so the app needs a one-off conversion using SQLCipher's own export tooling into a new encrypted file, then switches to that file.
saying these in an interview costs you the question
- useSQLCipher can be switched on in Expo Go or with a JavaScript update
- Setting PRAGMA key once stores it in the file for later launches
- Hard-coding the key in the JavaScript bundle is good enough
- Enabling SQLCipher encrypts the existing plaintext database automatically
- A key set on db also applies to the txn of withExclusiveTransactionAsync