iCloud key-value storage sync across devices
npm install @nauverse/expo-cloud-settings



An Expo module wrapping Apple's NSUbiquitousKeyValueStore for iCloud key-value sync across devices. Hooks-first React API with change listeners.
Android returns no-op (null values, isAvailable() === false) so you can use the same API on both platforms without crashes.
- React hooks: useCloudSetting, useCloudSettingBool, useCloudSettingNumber, useCloudSettingObject
- Shared context provider - one event listener, one cache, no duplicate native reads
- Change event listeners for cross-device sync
- Config plugin - no manual Xcode entitlement setup
- Typed helpers: setBool, setNumber, setObject
- clear() to remove all keys
- isAvailable() runtime platform check (checks iCloud sign-in status)
- Android no-op module (safe to call, returns null)
``bash`
npx expo install @nauverse/expo-cloud-settings
Add the config plugin to your app.config.ts (or app.json):
`ts`
export default {
plugins: ['@nauverse/expo-cloud-settings'],
};
This automatically adds the following entitlements to your iOS build:
- com.apple.developer.ubiquity-kvstore-identifier - iCloud Key-Value Storage identifiercom.apple.developer.icloud-container-identifiers
- - enables the iCloud capabilitycom.apple.developer.icloud-services
- - declares CloudKit service usage
These entitlements ensure that EAS Build (and Xcode) correctly enable the iCloud capability on your App ID and provisioning profile.
By default the KVS identifier is $(TeamIdentifierPrefix)$(CFBundleIdentifier) (Apple's recommended default). To share a KVS container across multiple apps:
`ts`
export default {
plugins: [
['@nauverse/expo-cloud-settings', { containerIdentifier: '$(TeamIdentifierPrefix)com.example.shared' }],
],
};
Wrap your app with CloudSettingsProvider. This sets up a single shared event listener and in-memory cache for all hooks:
`tsx
import { CloudSettingsProvider } from '@nauverse/expo-cloud-settings';
export default function App() {
return (
);
}
`
All useCloudSetting* hooks must be descendants of this provider.
`tsx
import { useCloudSetting, useCloudSettingBool, isAvailable } from '@nauverse/expo-cloud-settings';
function Settings() {
const [username, setUsername] = useCloudSetting('username', 'Guest');
const [darkMode, setDarkMode] = useCloudSettingBool('darkMode', false);
return (
);
}
`
Multiple components using the same key share the cached value and stay in sync automatically. When a value changes on another device, all hooks for that key re-render with the new value.
#### useCloudSetting(key, defaultValue?)
Returns readonly [string | null, (value: string | null) => void].
Setting to null removes the key.
#### useCloudSettingBool(key, defaultValue?)
Returns readonly [boolean | null, (value: boolean | null) => void].
#### useCloudSettingNumber(key, defaultValue?)
Returns readonly [number | null, (value: number | null) => void].
#### useCloudSettingObject
Returns readonly [T | null, (value: T | null) => void].
Values are JSON-serialized. Returns null (or defaultValue) if the stored value is not valid JSON.
`tsx
interface Preferences {
theme: string;
fontSize: number;
}
const [prefs, setPrefs] = useCloudSettingObject
theme: 'light',
fontSize: 16,
});
`
For use outside of React components (no provider required):
`ts
import {
getString, setString, remove, getAllKeys, clear, isAvailable,
getBool, setBool, getNumber, setNumber, getObject, setObject,
addChangeListener,
} from '@nauverse/expo-cloud-settings';
// String
setString('token', 'abc123');
const token = getString('token'); // 'abc123' | null
// Boolean
setBool('notifications', true);
const enabled = getBool('notifications'); // true | null
// Number
setNumber('launchCount', 5);
const count = getNumber('launchCount'); // 5 | null
// Object
setObject('user', { name: 'Alice', age: 30 });
const user = getObject<{ name: string; age: number }>('user');
// Keys & cleanup
const keys = getAllKeys(); // string[]
remove('token');
clear(); // removes all keys
// Platform check
if (isAvailable()) {
// iCloud KVS is available (iOS with iCloud signed in)
}
`
Listen for changes pushed from other devices:
`ts
import { addChangeListener } from '@nauverse/expo-cloud-settings';
const subscription = addChangeListener((event) => {
console.log('Changed keys:', event.changedKeys);
console.log('Reason:', event.reason);
// reason: 'serverChange' | 'initialSync' | 'quotaViolation' | 'accountChange'
});
// Clean up
subscription.remove();
`
The CloudSettingsProvider creates a single CloudSettingsStore that:
1. Maintains an in-memory cache of all read keys
2. Registers one native event listener for onStoreChangeduseSyncExternalStore
3. When a change event arrives, re-reads only the affected keys from native
4. Notifies all subscribed hooks via
This means:
- 10 components reading the same key = 1 native read (not 10)
- 1 event listener total (not 1 per hook)
- No race conditions between mount and subscription
| Limit | Value |
|-------|-------|
| Total storage | 1 MB |
| Maximum keys | 1024 |
| Per-key size | 1 MB |
Exceeding limits may trigger a quotaViolation change event and cause data loss.
iCloud KVS data is stored in the user's iCloud account and is not encrypted at the application level. Do not store sensitive data such as passwords, tokens, API keys, or personal health information. For sensitive data, use expo-secure-store or a server-side solution.
- Auto-syncs across devices signed into the same iCloud account
- Background sync when network is available
- The system coalesces writes and syncs periodically (not per-write)
- First sync after app launch may take a few moments
- Works offline - changes sync when connectivity is restored
- Sync does not work in the iOS Simulator (device-only)
| Platform | Status |
|----------|--------|
| iOS | Full support via NSUbiquitousKeyValueStore |null
| Android | No-op (returns , isAvailable() returns false) - real sync support coming soon via Google Drive App Data |
| Web | Not supported |
> Android support coming soon. The Android module currently acts as a safe no-op so your code works on both platforms without crashes. Real cross-device sync on Android (via Google Drive App Data) is on the roadmap. Follow the repo for updates.
`ts
type CloudSettingsChangeReason =
| 'serverChange' // Another device changed values
| 'initialSync' // First sync after app launch
| 'quotaViolation' // Storage limit exceeded
| 'accountChange'; // iCloud account changed
type CloudSettingsChangeEvent = {
readonly changedKeys: ReadonlyArray
readonly reason: CloudSettingsChangeReason;
};
``
Big thanks to https://github.com/okwasniewski/expo-icloud-storage I was initially using that repository in my projects and it was really great. This repository started because I needed specific changes (like the hooks and Android support) and creating a merge request there would change the project a lot, but if not, I would have just done that. Again, thanks for the inspiration!
MIT