{"id":48362094,"url":"https://github.com/TheNaubit/expo-cloud-settings","last_synced_at":"2026-04-21T03:01:24.308Z","repository":{"id":336874885,"uuid":"1151444258","full_name":"TheNaubit/expo-cloud-settings","owner":"TheNaubit","description":"iCloud key-value storage sync across devices in Expo","archived":false,"fork":false,"pushed_at":"2026-02-08T16:34:07.000Z","size":759,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-03T20:57:12.063Z","etag":null,"topics":["cloud-stora","cloudkit","cross-device","expo","expo-cloud-settings","expocloudsettings","google-drive","icloud","key-value-storage","react-native","sync"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TheNaubit.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-06T13:27:36.000Z","updated_at":"2026-02-07T13:26:49.000Z","dependencies_parsed_at":null,"dependency_job_id":"59d18f30-ae52-4ca8-bad6-128b28404689","html_url":"https://github.com/TheNaubit/expo-cloud-settings","commit_stats":null,"previous_names":["thenaubit/expo-cloud-settings"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/TheNaubit/expo-cloud-settings","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-cloud-settings","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-cloud-settings/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-cloud-settings/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-cloud-settings/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TheNaubit","download_url":"https://codeload.github.com/TheNaubit/expo-cloud-settings/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TheNaubit%2Fexpo-cloud-settings/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32074812,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-21T02:38:07.213Z","status":"ssl_error","status_checked_at":"2026-04-21T02:38:06.559Z","response_time":128,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloud-stora","cloudkit","cross-device","expo","expo-cloud-settings","expocloudsettings","google-drive","icloud","key-value-storage","react-native","sync"],"created_at":"2026-04-05T13:00:29.644Z","updated_at":"2026-04-21T03:01:24.296Z","avatar_url":"https://github.com/TheNaubit.png","language":"TypeScript","readme":"# expo-cloud-settings\n\n[![npm version](https://img.shields.io/npm/v/@nauverse/expo-cloud-settings)](https://www.npmjs.com/package/@nauverse/expo-cloud-settings)\n[![CI](https://github.com/TheNaubit/expo-cloud-settings/actions/workflows/ci.yml/badge.svg)](https://github.com/TheNaubit/expo-cloud-settings/actions/workflows/ci.yml)\n[![Release](https://github.com/TheNaubit/expo-cloud-settings/actions/workflows/release.yml/badge.svg)](https://github.com/TheNaubit/expo-cloud-settings/actions/workflows/release.yml)\n[![License: MIT](https://img.shields.io/npm/l/@nauverse/expo-cloud-settings)](https://github.com/TheNaubit/expo-cloud-settings/blob/main/LICENSE)\n\nAn Expo module wrapping Apple's `NSUbiquitousKeyValueStore` for iCloud key-value sync across devices. Hooks-first React API with change listeners.\n\nAndroid returns no-op (null values, `isAvailable() === false`) so you can use the same API on both platforms without crashes.\n\n## Features\n\n- React hooks: `useCloudSetting`, `useCloudSettingBool`, `useCloudSettingNumber`, `useCloudSettingObject`\n- Shared context provider - one event listener, one cache, no duplicate native reads\n- Change event listeners for cross-device sync\n- Config plugin - no manual Xcode entitlement setup\n- Typed helpers: `setBool`, `setNumber`, `setObject\u003cT\u003e`\n- `clear()` to remove all keys\n- `isAvailable()` runtime platform check (checks iCloud sign-in status)\n- Android no-op module (safe to call, returns null)\n\n## Installation\n\n```bash\nnpx expo install @nauverse/expo-cloud-settings\n```\n\nAdd the config plugin to your `app.config.ts` (or `app.json`):\n\n```ts\nexport default {\n  plugins: ['@nauverse/expo-cloud-settings'],\n};\n```\n\nThis automatically adds the following entitlements to your iOS build:\n\n- `com.apple.developer.ubiquity-kvstore-identifier` - iCloud Key-Value Storage identifier\n- `com.apple.developer.icloud-container-identifiers` - enables the iCloud capability\n- `com.apple.developer.icloud-services` - declares CloudKit service usage\n\nThese entitlements ensure that EAS Build (and Xcode) correctly enable the iCloud capability on your App ID and provisioning profile.\n\n### Custom container identifier\n\nBy default the KVS identifier is `$(TeamIdentifierPrefix)$(CFBundleIdentifier)` (Apple's recommended default). To share a KVS container across multiple apps:\n\n```ts\nexport default {\n  plugins: [\n    ['@nauverse/expo-cloud-settings', { containerIdentifier: '$(TeamIdentifierPrefix)com.example.shared' }],\n  ],\n};\n```\n\n## Setup\n\nWrap your app with `CloudSettingsProvider`. This sets up a single shared event listener and in-memory cache for all hooks:\n\n```tsx\nimport { CloudSettingsProvider } from '@nauverse/expo-cloud-settings';\n\nexport default function App() {\n  return (\n    \u003cCloudSettingsProvider\u003e\n      \u003cMyApp /\u003e\n    \u003c/CloudSettingsProvider\u003e\n  );\n}\n```\n\nAll `useCloudSetting*` hooks must be descendants of this provider.\n\n## Usage\n\n### Hooks (recommended)\n\n```tsx\nimport { useCloudSetting, useCloudSettingBool, isAvailable } from '@nauverse/expo-cloud-settings';\n\nfunction Settings() {\n  const [username, setUsername] = useCloudSetting('username', 'Guest');\n  const [darkMode, setDarkMode] = useCloudSettingBool('darkMode', false);\n\n  return (\n    \u003cView\u003e\n      \u003cText\u003eiCloud available: {String(isAvailable())}\u003c/Text\u003e\n      \u003cText\u003eHello, {username}\u003c/Text\u003e\n      \u003cButton title=\"Toggle dark mode\" onPress={() =\u003e setDarkMode(!darkMode)} /\u003e\n      \u003cButton title=\"Clear username\" onPress={() =\u003e setUsername(null)} /\u003e\n    \u003c/View\u003e\n  );\n}\n```\n\nMultiple 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.\n\n#### `useCloudSetting(key, defaultValue?)`\n\nReturns `readonly [string | null, (value: string | null) =\u003e void]`.\n\nSetting to `null` removes the key.\n\n#### `useCloudSettingBool(key, defaultValue?)`\n\nReturns `readonly [boolean | null, (value: boolean | null) =\u003e void]`.\n\n#### `useCloudSettingNumber(key, defaultValue?)`\n\nReturns `readonly [number | null, (value: number | null) =\u003e void]`.\n\n#### `useCloudSettingObject\u003cT\u003e(key, defaultValue?)`\n\nReturns `readonly [T | null, (value: T | null) =\u003e void]`.\n\nValues are JSON-serialized. Returns `null` (or `defaultValue`) if the stored value is not valid JSON.\n\n```tsx\ninterface Preferences {\n  theme: string;\n  fontSize: number;\n}\n\nconst [prefs, setPrefs] = useCloudSettingObject\u003cPreferences\u003e('prefs', {\n  theme: 'light',\n  fontSize: 16,\n});\n```\n\n### Functional API\n\nFor use outside of React components (no provider required):\n\n```ts\nimport {\n  getString, setString, remove, getAllKeys, clear, isAvailable,\n  getBool, setBool, getNumber, setNumber, getObject, setObject,\n  addChangeListener,\n} from '@nauverse/expo-cloud-settings';\n\n// String\nsetString('token', 'abc123');\nconst token = getString('token'); // 'abc123' | null\n\n// Boolean\nsetBool('notifications', true);\nconst enabled = getBool('notifications'); // true | null\n\n// Number\nsetNumber('launchCount', 5);\nconst count = getNumber('launchCount'); // 5 | null\n\n// Object\nsetObject('user', { name: 'Alice', age: 30 });\nconst user = getObject\u003c{ name: string; age: number }\u003e('user');\n\n// Keys \u0026 cleanup\nconst keys = getAllKeys(); // string[]\nremove('token');\nclear(); // removes all keys\n\n// Platform check\nif (isAvailable()) {\n  // iCloud KVS is available (iOS with iCloud signed in)\n}\n```\n\n### Change listener\n\nListen for changes pushed from other devices:\n\n```ts\nimport { addChangeListener } from '@nauverse/expo-cloud-settings';\n\nconst subscription = addChangeListener((event) =\u003e {\n  console.log('Changed keys:', event.changedKeys);\n  console.log('Reason:', event.reason);\n  // reason: 'serverChange' | 'initialSync' | 'quotaViolation' | 'accountChange'\n});\n\n// Clean up\nsubscription.remove();\n```\n\n## Architecture\n\nThe `CloudSettingsProvider` creates a single `CloudSettingsStore` that:\n\n1. Maintains an in-memory cache of all read keys\n2. Registers one native event listener for `onStoreChanged`\n3. When a change event arrives, re-reads only the affected keys from native\n4. Notifies all subscribed hooks via `useSyncExternalStore`\n\nThis means:\n- 10 components reading the same key = 1 native read (not 10)\n- 1 event listener total (not 1 per hook)\n- No race conditions between mount and subscription\n\n## iCloud KVS limits\n\n| Limit | Value |\n|-------|-------|\n| Total storage | 1 MB |\n| Maximum keys | 1024 |\n| Per-key size | 1 MB |\n\nExceeding limits may trigger a `quotaViolation` change event and cause data loss.\n\n## Security\n\niCloud 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.\n\n## Sync behavior\n\n- Auto-syncs across devices signed into the same iCloud account\n- Background sync when network is available\n- The system coalesces writes and syncs periodically (not per-write)\n- First sync after app launch may take a few moments\n- Works offline - changes sync when connectivity is restored\n- Sync does not work in the iOS Simulator (device-only)\n\n## Platform support\n\n| Platform | Status |\n|----------|--------|\n| iOS | Full support via `NSUbiquitousKeyValueStore` |\n| Android | No-op (returns `null`, `isAvailable()` returns `false`) - **real sync support coming soon** via Google Drive App Data |\n| Web | Not supported |\n\n\u003e **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.\n\n## API reference\n\n### Types\n\n```ts\ntype CloudSettingsChangeReason =\n  | 'serverChange'    // Another device changed values\n  | 'initialSync'     // First sync after app launch\n  | 'quotaViolation'  // Storage limit exceeded\n  | 'accountChange';  // iCloud account changed\n\ntype CloudSettingsChangeEvent = {\n  readonly changedKeys: ReadonlyArray\u003cstring\u003e;\n  readonly reason: CloudSettingsChangeReason;\n};\n```\n\n## Acknowledgements\n\nBig 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!\n\n## License\n\nMIT\n","funding_links":[],"categories":["Storage \u0026 Database"],"sub_categories":["Graphics \u0026 Drawing"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTheNaubit%2Fexpo-cloud-settings","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FTheNaubit%2Fexpo-cloud-settings","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTheNaubit%2Fexpo-cloud-settings/lists"}