expo-icloud-kv
v0.1.2
Published
Async key/value storage for React Native / Expo backed by iCloud (NSUbiquitousKeyValueStore) — sync small pieces of app state across a user's Apple devices with a Promise-based API.
Maintainers
Readme
expo-icloud-kv
Features
Promise API:getItem,setItem,removeItem,getAllKeys,synchronize— same shape asAsyncStorage, so migration is a search-and-replace.
Cross-device sync:- Values written on iPhone show up on iPad and Mac within seconds (typically) or minutes (worst case) — no server, no accounts, no code.
External change events:- Subscribe to
onExternalChangeand react when another device writes, when the initial sync completes, when the user switches iCloud accounts, or when you hit the quota.
- Subscribe to
Config plugin included:- Add one line to
app.jsonand thecom.apple.developer.ubiquity-kvstore-identifierentitlement is wired up for you — no manual Xcode surgery.
- Add one line to
Cross-platform safe:- Every call is a no-op on Android (
getItemresolvesnull,getAllKeysresolves[], etc.) so you can call it unconditionally from shared code.
- Every call is a no-op on Android (
Expo Modules API:- Ships as a modern Expo Module — no legacy
.mbridge, no manual linking. Justnpx expo installand go.
- Ships as a modern Expo Module — no legacy
Installation
npx expo install expo-icloud-kvThen rebuild your dev client (this package uses native code, so it is not available in Expo Go):
npx expo prebuild
npx expo run:iosSetup
Add the config plugin to your app.json:
{
"expo": {
"plugins": [
[
"expo-icloud-kv",
{
"identifier": "$(TeamIdentifierPrefix)com.example.myapp"
}
]
]
}
}
identifier— required. The value of thecom.apple.developer.ubiquity-kvstore-identifierentitlement. The$(TeamIdentifierPrefix)prefix is a build-time variable that Xcode expands to your Apple Team ID plus a dot. In most projects you'll want the identifier to end with your app's bundle ID, e.g."$(TeamIdentifierPrefix)com.example.myapp".
containers— optionalstring[]. Set this only if your app also uses CloudKit / iCloud Documents. Leaving it empty ([]) is the correct default for pure key/value sync; the plugin writes an empty array automatically.
services— optionalstring[]. Set to e.g.["CloudKit"]if you want the plugin to also enable full iCloud services. Omit for pure KV sync.
You also need to enable the iCloud → Key-value storage capability on your App ID in the Apple Developer portal. Expo cannot do this for you — do it once in the Apple Developer console when you register the bundle ID.
Usage
import * as ICloudKV from 'expo-icloud-kv';
// Write
await ICloudKV.setItem('pinnedGroups', JSON.stringify(['410101', '410102']));
// Read
const raw = await ICloudKV.getItem('pinnedGroups');
const pinned: string[] = raw ? JSON.parse(raw) : [];
// Force a sync (optional — sets/removes already sync automatically)
await ICloudKV.synchronize();
// Listen for changes made on other devices
const sub = ICloudKV.addExternalChangeListener(({ keys, reason }) => {
console.log('iCloud pushed changes:', keys, 'reason:', reason);
});
// Later:
sub.remove();API reference
| Function | Signature | Description |
| -------- | --------- | ----------- |
| getItem | (key: string) => Promise<string \| null> | Read a string. Resolves null when missing or on non-iOS. |
| setItem | (key: string, value: string) => Promise<void> | Write a string and request a sync. |
| removeItem | (key: string) => Promise<void> | Remove a key and request a sync. |
| getAllKeys | () => Promise<string[]> | List every key currently in the store. |
| synchronize | () => Promise<boolean> | Force a sync. false = entitlement missing / user signed out of iCloud. |
| addExternalChangeListener | (cb: (event) => void) => EventSubscription | Subscribe to remote changes. Call .remove() to unsubscribe. |
| isAvailable | boolean | true on iOS, false elsewhere — useful for gating UI. |
type ExternalChangeReason =
| 'server' // A change on another device propagated to this one.
| 'initialSync' // First sync after launch or account change.
| 'quotaViolation' // Exceeded 1 MB total / 1024 keys / 1 MB per value.
| 'accountChange' // User signed in/out of iCloud, or switched accounts.
| 'unknown';
interface ExternalChangeEvent {
keys: string[];
reason: ExternalChangeReason;
}Requirements
- iOS 14.0+.
- Expo SDK 54+ (uses Expo Modules API).
- Xcode 15+.
- Apple Developer account with the iCloud → Key-value storage capability enabled on the App ID.
Non-iOS calls are silent no-ops.
Limits
iCloud KV is designed for small user preferences, not app data:
- 1 MB total storage per app.
- 1024 keys maximum.
- 1 MB per value (large blobs should live in CloudKit or iCloud Documents).
- Sync is best-effort. Expect seconds to minutes of propagation delay — do not treat it as a strong-consistency store.
If you exceed the quota, you'll receive an onExternalChange event with reason: 'quotaViolation' and the newest writes will silently drop.
How it works
The module is a thin Swift wrapper around NSUbiquitousKeyValueStore.default using the Expo Modules API — no legacy RCTBridgeModule or Objective-C shim.
The config plugin adds two entries to <AppName>.entitlements:
com.apple.developer.ubiquity-kvstore-identifier— the identifier you pass inapp.json.com.apple.developer.icloud-container-identifiers— an empty array (the OS requires the key even if you're not using CloudKit).
External change notifications are delivered via NSUbiquitousKeyValueStore.didChangeExternallyNotification; the module subscribes automatically as soon as JS registers its first listener and unsubscribes on the last remove().
Contributing
The example/ directory contains a minimal Expo app that exercises every API.
git clone https://github.com/kostyabet/expo-icloud-kv
cd expo-icloud-kv
npm install
npm run build
cd example
npm install
npx expo run:iosPull requests welcome — please add a CHANGELOG.md entry.
Source code
GitHub repo
License
MIT
Support
Press star on our GitHub repo please!
