expo-block-store
v1.0.0
Published
Expo module wrapping the Android Block Store API (Google Play Services) — persist small tokens or identifiers that survive app uninstall/reinstall and transfer to a new device, with no sign-in. iOS and web are safe no-ops.
Maintainers
Readme
expo-block-store
An Expo module that wraps the Android Block Store API (Google Play services). Store small tokens or identifiers that survive app uninstall/reinstall and transfer to a new device during setup — with no sign-in screen.
Built with the Expo Modules API, so it works with both the New Architecture and the legacy bridge. iOS and web are safe no-ops (no native code, calls resolve to neutral defaults), so the same call sites compile and run everywhere without Platform branching.
Why
Apps without traditional auth often use a client-generated id as the user's identity. On iOS that id can be recovered from StoreKit transaction history or the Keychain (both survive uninstall). On Android there is no equivalent: BillingClient only returns active purchases, and Keystore/SharedPreferences are wiped on uninstall. Block Store is Google's purpose-built primitive for exactly this — a tiny, app-private, optionally end-to-end-encrypted blob that rides the Android Backup infrastructure.
| Source | Survives same-device reinstall | Survives new device |
| --- | --- | --- |
| SharedPreferences / MMKV / expo-secure-store (Android) | ❌ | ❌ |
| Play Billing obfuscatedAccountId | only while a purchase is active | only with an active purchase |
| Block Store | ✅ (Backup enabled) | ✅ (cloud / device-to-device restore) |
Installation
npx expo install expo-block-storeThen rebuild the native project (Continuous Native Generation):
npx expo prebuild --cleanIt pulls in com.google.android.gms:play-services-auth-blockstore. No manifest changes or extra setup required.
Quick start
import * as BlockStore from 'expo-block-store';
// Persist an anonymous user id so it survives reinstall.
await BlockStore.setItem('com.myapp.userId', userId, { shouldBackupToCloud: true });
// On next launch / after reinstall — recover it.
const recovered = await BlockStore.getItem('com.myapp.userId');
// → "550E8400-E29B-41D4-A716-446655440000" | nullAPI
All functions are async. On iOS/web they resolve to the neutral values noted below and never throw. On Android the read/write promises may reject (size limit, entry-count limit, Play services unavailable) — handle that at the call site.
setItem(key, value, options?) => Promise<boolean>
Stores value (a string, ≤ MAX_ENTRY_SIZE bytes UTF-8) under key. Resolves true on success. iOS/web: false.
await BlockStore.setItem('com.myapp.userId', userId, { shouldBackupToCloud: true });options.shouldBackupToCloud (default false): also back the entry up to the cloud so it can be restored on a new device. Requires the user's Google Backup to be enabled and is end-to-end encrypted when the device supports it.
⚠️ Cloud gotcha: if you later call
setItemfor the same data withoutshouldBackupToCloud: true, the previously cloud-backed bytes are deleted from the cloud on the next sync. Set the flag consistently.
getItem(key) => Promise<string | null>
Reads the string stored under key, or null if absent. iOS/web: null.
getAllItems() => Promise<Record<string, string>>
Reads every entry this app stored as a key → value map. iOS/web: {}.
removeItem(key) => Promise<boolean>
Deletes the entry under key. Resolves true if something was deleted. iOS/web: false.
clear() => Promise<boolean>
Deletes every entry this app stored. iOS/web: false.
isEndToEndEncryptionAvailable() => Promise<boolean>
Whether data stored right now will be end-to-end encrypted (Android 9+ with a device screen lock). Use it to decide whether to enable cloud backup. iOS/web: false.
const e2ee = await BlockStore.isEndToEndEncryptionAvailable();
await BlockStore.setItem(key, value, { shouldBackupToCloud: e2ee });isSupported() => boolean
true only on Android with the native module linked. Use it to branch recovery logic.
Constants
MAX_ENTRY_SIZE—4096(bytes per entry)MAX_ENTRY_COUNT—16(entries per app)
How it works
Block Store keeps app-private blobs on top of the Android Backup system. Data is readable only by the same app (verified via package name + signing signature).
- Same device, uninstall → reinstall: restored if the user has Backup enabled (Settings → Google → Backup).
- New device: transferred via device-to-device restore or cloud restore during setup (requires
shouldBackupToCloud: true).
| Capability | Minimum | | --- | --- | | Block Store (store/retrieve/delete) | Android 6 (API 23) + Google Play services | | End-to-end encryption | Android 9 (API 29) + screen lock | | Cloud restore (new device) | Pixel: Android 9 (API 29) · others: Android 12 (API 31) |
Caveats
- Backup toggle: persistence depends on the user's Google Backup being on. It's on by default for most signed-in users, but not guaranteed — treat Block Store as one recovery layer, not the sole source of truth.
- Capacity: up to 16 entries × 4 KB. Plenty for ids/tokens, not for documents.
- Not for secrets you can't rotate. It's a recoverable identifier store; pair sensitive tokens with server-side validation.
Platform behavior
| Platform | Backend |
| --- | --- |
| Android | Google Play services Block Store |
| iOS | no-op (resolves null/false/{}) |
| Web | no-op (resolves null/false/{}) |
On iOS, recovering an anonymous id is typically already covered by StoreKit transaction history or the Keychain, so this module focuses on closing the Android gap.
License
MIT © Dmitry Matatov
