https://github.com/TheNaubit/expo-cloud-settings
iCloud key-value storage sync across devices in Expo
https://github.com/TheNaubit/expo-cloud-settings
cloud-stora cloudkit cross-device expo expo-cloud-settings expocloudsettings google-drive icloud key-value-storage react-native sync
Last synced: 6 days ago
JSON representation
iCloud key-value storage sync across devices in Expo
- Host: GitHub
- URL: https://github.com/TheNaubit/expo-cloud-settings
- Owner: TheNaubit
- License: mit
- Created: 2026-02-06T13:27:36.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-02-08T16:34:07.000Z (3 months ago)
- Last Synced: 2026-03-03T20:57:12.063Z (about 2 months ago)
- Topics: cloud-stora, cloudkit, cross-device, expo, expo-cloud-settings, expocloudsettings, google-drive, icloud, key-value-storage, react-native, sync
- Language: TypeScript
- Homepage:
- Size: 741 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- awesome-react-native - expo-cloud-settings - iCloud key-value synchronization across iOS devices using React hooks. (Storage & Database / Graphics & Drawing)
README
# expo-cloud-settings
[](https://www.npmjs.com/package/@nauverse/expo-cloud-settings)
[](https://github.com/TheNaubit/expo-cloud-settings/actions/workflows/ci.yml)
[](https://github.com/TheNaubit/expo-cloud-settings/actions/workflows/release.yml)
[](https://github.com/TheNaubit/expo-cloud-settings/blob/main/LICENSE)
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.
## Features
- 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)
## Installation
```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 identifier
- `com.apple.developer.icloud-container-identifiers` - enables the iCloud capability
- `com.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.
### Custom container identifier
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' }],
],
};
```
## Setup
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.
## Usage
### Hooks (recommended)
```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 (
iCloud available: {String(isAvailable())}
Hello, {username}
setDarkMode(!darkMode)} />
setUsername(null)} />
);
}
```
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(key, defaultValue?)`
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('prefs', {
theme: 'light',
fontSize: 16,
});
```
### Functional API
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)
}
```
### Change listener
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();
```
## Architecture
The `CloudSettingsProvider` creates a single `CloudSettingsStore` that:
1. Maintains an in-memory cache of all read keys
2. Registers one native event listener for `onStoreChanged`
3. When a change event arrives, re-reads only the affected keys from native
4. Notifies all subscribed hooks via `useSyncExternalStore`
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
## iCloud KVS limits
| 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.
## Security
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.
## Sync behavior
- 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 support
| Platform | Status |
|----------|--------|
| iOS | Full support via `NSUbiquitousKeyValueStore` |
| Android | No-op (returns `null`, `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.
## API reference
### Types
```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;
};
```
## Acknowledgements
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!
## License
MIT