Skip to content

Latest commit

Β 

History

51 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

npm version npm downloads license platforms TypeScript Expo compatible New Architecture supported Legacy Architecture supported Sponsor this project

react-native-cloud-sync

iCloud key-value store, CloudKit, iCloud Drive and Google Drive behind one API - iOS, Android, web, both architectures.

Documentation Β· Comparison Β· API Β· Platform Notes


✨ Features

  • ☁️ Four providers behind one API: NSUbiquitousKeyValueStore (Apple's small key-value iCloud store), CloudKit records, iCloud Drive documents and Google Drive appDataFolder. Use them directly or through one facade.
  • πŸ”„ Auto-sync between iOS and Android in both directions: mirror writes to iCloud and Drive together, resolve reads back whichever copy is newest, so either platform can be where the user started.
  • 🍏 CloudKit reaches Android and web too, via CloudKit Web Services, against the same private database your iOS app uses.
  • πŸ“‚ icloudDocuments writes into the user's own iCloud Drive, so the files show up in Files.app.
  • 🚨 Every failure is a typed rejection (ERR_NOT_SIGNED_IN, ERR_QUOTA_EXCEEDED, ERR_RATE_LIMITED with retryAfterMs, ...); null means only "key doesn't exist".
  • πŸ‘€ All five CKAccountStatus values surface as-is, plus onAccountChange with identityChanged, which drops the previous account's caches and queued writes.
  • πŸ”” onRemoteChange fires on every provider and the facade, including Google Drive via its change cursor.
  • πŸ“¦ Small values go to the key-value store, larger ones to a CKRecord field, binary to a CKAsset or a resumable Drive upload. The size check picks the target.
  • πŸ” Retryable failures queue into a durable outbox: backoff honours retry hints, auto-drains on foreground, bounded, never overwrites a newer write.
  • 🧺 multiGet/multiSet/multiRemove/clear batch for real, one request per provider.
  • πŸͺ React hooks from /hooks: useCloudItem, useCloudItems, useCloudCollection, useAccountStatus, usePendingWrites. They drop stale responses and never setState after unmount.
  • πŸ” cloudKitEncrypted uses CloudKit's own encryptedValues, so only ciphertext leaves the device; every other provider has a codec seam for your own cipher.
  • πŸ§ͺ An in-memory provider with fault injection, plus the native mock, both exported (/testing, /jest-mock), so every failure path is testable in Jest.
  • βš™οΈ React Native 0.71 through 0.86+, old and new architecture, with the #ifdef bridge for the legacy one.

πŸ’‘ Why?

Cloud storage in React Native is fragmented into single-provider wrappers repeating the same defects: two ship a setItem that reports a failed write as a success (one checks the wrong error variable, the other discards the result entirely), and a third flattens five iCloud account states into one boolean.

A catch { return null } makes "not signed in", "offline", "out of storage" and "no such key" indistinguishable. The app can't tell the user anything useful, or decide whether to retry.

This library was built error contract first, providers second.

πŸ“š Upstream documentation

This package is a wrapper. When something behaves unexpectedly, the answer is usually in Apple's or Google's docs.

Apple

NSUbiquitousKeyValueStore The iCloud key-value store; 1 MB / 1024-key limits
CloudKit Β· CKDatabase Β· CKRecord Records in the user's private database
CKAsset Β· CKRecordZone Binary assets and custom zones
CKAccountStatus The five account states this package surfaces verbatim
CloudKit Web Services The REST API behind the Android and web paths
Authentication Β· Data size limits Β· Error codes Worth reading before shipping CloudKit on Android
iCloud entitlements The keys the config plugin writes
CloudKit Console Where containers, schemas, API tokens and the sign-in callback live

Google

The appDataFolder The hidden per-app folder this package stores into
Drive files resource The REST endpoints behind the provider
Drive API scopes Why the scope is drive.appdata

βš–οΈ Comparison

this kuatsu/
cloud-storage
icloud-kit expo-cloudkit okwasniewski/
icloud-storage
cloudkit-
storage
iCloud key-value store βœ… βœ… βœ… - βœ… -
CloudKit records βœ… - βœ… βœ… - βœ…
Google Drive βœ… βœ… - - - -
iOS βœ… βœ… βœ… βœ… βœ… βœ…
Android βœ… βœ… - ❌ [1] - -
Web βœ… partial [2] - - - -
CloudKit on Android/web βœ… - - ❌ [1] - -
New Architecture βœ… βœ… βœ… βœ… βœ… -
Legacy Architecture βœ… ❌ βœ… [3] βœ… [3] βœ… [3] βœ…
Typed error codes βœ… - partial βœ… - -
5-value account status βœ… boolean boolean βœ… - -
Identity-change event βœ… ⚠️ [4] - βœ… - -
Remote-change event βœ… βœ… - n/a ❌ [5] βœ…
Offline write queue βœ… - - βœ… - -
Size tiering βœ… - - - - -
Binary / assets βœ… [9] βœ… ❌ [6] βœ… - -
User-visible iCloud Drive files βœ… - - - βœ… -
Native end-to-end encryption βœ… - - - - -
Batch operations βœ… - - - - -
React hooks βœ… βœ… - - - -
Encryption seam βœ… - - - - -
Exported test harness βœ… - - ⚠️ [7] - -
Mac Catalyst βœ… - - ⚠️ - -
Actively maintained βœ… βœ… βœ… ❌ [8] ❌ [5] ❌

[1] expo-cloudkit's README: Android throws CloudKitNotSupportedError on every call.
[2] Google Drive support is text-based only.
[3] Via the Expo Modules API; pulls in expo-modules-core.
[4] Fires before JS binds the emitter, crashing with std::bad_function_call (SIGABRT); hit three times.
[5] One version ever shipped; the change-listener PR has sat open since February.
[6] Field type is string | number | null, so it can't hold binary data.
[7] A mock factory exists, but isn't exported or documented.
[8] No commits since April; the last four releases shipped with Swift that didn't compile.
[9] CKAsset on Apple platforms; CloudKit Web Services' upload-token protocol on Android/web, capped at 15 MB by CloudKit. Use googleDriveFiles for anything bigger; it chunks and resumes the same way.

πŸ“– Documentation

Choosing a provider Which one, what it costs, why to let the user pick
iCloud key-value store Small settings, zero friction, Apple only
CloudKit Records, zones, assets, and the Android/web path
iCloud Drive Files in the user's own Drive, visible in Files.app
Google Drive The always-on cross-platform backend
The store facade Tiering, outbox, migration, fallthrough
Error handling The typed contract
Encryption What's encrypted for you, and how to add your own
React hooks Binding cloud state to components
Recipes Backup/restore, migration, offline-first
Testing Fault injection without a device
API reference Every export
Platform notes Entitlements, architectures, build config
Troubleshooting Common problems and what they usually mean

πŸ“‹ Requirements

Minimum
React Native 0.71
iOS 15.1
Node 20

Both architectures supported; RN 0.82 removed Legacy Architecture, so that half only matters on 0.81 and below.

Platform support per provider

iOS / macOS Android Web
icloudKV native - -
cloudKit native REST REST
cloudKitEncrypted native - -
icloudDocuments native - -
googleDrive REST REST REST

An unavailable provider rejects with ERR_UNSUPPORTED_PLATFORM instead of silently doing nothing. Choosing a provider covers the trade-offs.

πŸ“¦ Installation

npx expo install react-native-cloud-sync

Add the config plugin, then rebuild:

{
  "expo": {
    "plugins": [
      ["react-native-cloud-sync", {
        "containerIdentifier": "iCloud.com.your.app"
      }]
    ]
  }
}

Bare React Native and manual ios/ entitlement keys: see Installation.

πŸš€ Quick start

iCloud key-value store

No sign-in and no UI; it uses the device's existing account.

import { icloudKV } from 'react-native-cloud-sync'

await icloudKV.setItem('settings/theme', 'dark')
const theme = await icloudKV.getItem('settings/theme')
// null means the key does not exist. Nothing else returns null.

icloudKV.onRemoteChange(({ keys }) => reload(keys))

Full guide

CloudKit

The same private database from iOS, Android and web.

import { cloudKit, cloudKitAssets } from 'react-native-cloud-sync'

await cloudKit.setItem('playlist', JSON.stringify(tracks))
const raw = await cloudKit.getItem('playlist')

// Anything above the 1 MB record limit goes in as a streamed CKAsset.
await cloudKitAssets.save({ recordName: 'avatar', fieldName: 'image', fileUri })

Android/web needs an Apple ID sign-in, and that token lasts up to two weeks, which suits an explicit import rather than background sync. Full guide

Sensitive data

CloudKit's own end-to-end encryption: Apple stores ciphertext and holds no key.

import { cloudKitEncrypted } from 'react-native-cloud-sync'

await cloudKitEncrypted.setItem('auth.refreshToken', token)

Apple-only: the key lives in the user's iCloud Keychain, so nothing server-side can decrypt it. Cross-platform needs the store's codec seam instead. Full guide

iCloud Drive

Files the user can open in Files.app.

import { icloudDocuments } from 'react-native-cloud-sync'

await icloudDocuments.save({ fileUri: localPath, name: 'Export 2024.csv' })

// A listed file may be a placeholder with no local bytes. fetch() downloads it.
const path = await icloudDocuments.fetch({ name: 'Export 2024.csv' })

Full guide

Google Drive

Identical behaviour on every platform, no periodic re-auth.

import { configureGoogleDrive, googleDrive } from 'react-native-cloud-sync'

configureGoogleDrive({
  getAccessToken: async () => (await GoogleSignin.getTokens()).accessToken,
})

await googleDrive.setItem('playlist.json', JSON.stringify(tracks))

Full guide

All of them at once

import { createCloudStore } from 'react-native-cloud-sync'

const store = createCloudStore({
  providers: ['icloudKV', 'googleDrive'],       // preference order
  writeMode: 'mirror',                          // write to both providers, not only the preferred one
  resolve: resolveByTimestamp('updatedAt'),     // read whichever copy is newest
  tiering: 'auto',                              // route by size
  outboxStorage: mmkvAdapter,                   // survive restarts
})

await store.setItem('playlist', json)
await store.flushOutbox()                       // on reconnect

Those two options make sync work in both directions across a mixed fleet: mirror copies to Drive so non-Apple devices can read it; resolve stops an Apple device serving a stale iCloud copy without checking Drive. Full guide

In a component

import { useCloudItem } from 'react-native-cloud-sync/hooks'

const { value, setValue, loading, error } = useCloudItem<Settings>(store, 'settings')

Re-reads on remote writes, drops stale responses, never calls setState after unmount. Full guide

Handling failures

import { isRetryable, requiresUserAction } from 'react-native-cloud-sync'

try {
  await store.setItem('k', 'v')
} catch (e) {
  if (requiresUserAction(e)) promptUser(e.code)      // signed out, out of storage
  else if (isRetryable(e)) scheduleRetry(e.retryAfterMs)
}

Full guide

πŸ§ͺ Testing

import { ErrorCode } from 'react-native-cloud-sync'
import { createMemoryProvider } from 'react-native-cloud-sync/testing'

const provider = createMemoryProvider({
  faults: { setItem: { code: ErrorCode.QUOTA_EXCEEDED } },
})

provider.emitAccountChange({ status: 'available', identityChanged: true })

Signed-out, offline and account-switch paths, all in Jest without a device. Full guide

πŸ§ͺ Example App

A playground covering every API, plus a live sync demo for side-by-side recording:

cd example
yarn installDevBuild:ios     # or :android
yarn start:web

Tabs: Sync (shared counter, one-tap mirror mode to iCloud+Drive), iCloud KV, CloudKit, Drive, Files (large-file backup/restore via GoogleDriveFileAdapter), Store, Faults.

Sync demo tab - shared counter with mirror provider selectionΒ Β iCloud key-value store tabΒ Β CloudKit tab - record read/write and oversized-write handlingΒ Β Google Drive tab - OAuth token configuration and file operationsΒ Β Store facade tab - size tiering across providers

🀝 Contributing

Issues and pull requests welcome. Run yarn lint, yarn typecheck and yarn test before opening one.

πŸ‘₯ Authors

πŸ“„ License

MIT

About

iCloud key-value store, CloudKit, iCloud Drive and Google Drive under one API - iOS, Android and Web, old and new architecture

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages