npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-cloudkit-bridge

v0.1.0

Published

Thin, stateless CloudKit private-database bridge for Expo apps: records, custom zone, change tokens, and the iCloud key-value store. Bring your own merge strategy.

Readme

expo-cloudkit-bridge

A thin, stateless CloudKit private-database bridge for Expo apps: batch record saves with first-to-server conflict arbitration, change-token delta fetches, account status and identity, and the iCloud key-value store with change events. About 300 lines of Swift; everything else is yours.

Extracted from Hedge, a shipped App Store app that uses it to sync play history across devices with no backend, no accounts, and a privacy label that still says "Data Not Collected" — the data lives in each user's own private iCloud, hosted by Apple under their Apple ID, unreadable by the developer.

This is a bridge, not a sync engine. It moves records and tells you about conflicts; your app owns the merge strategy, the outbox, and all persistence (even the change token round-trips through JS as base64). If you want the production-proven merge recipe this bridge was built for — an append-only fact log with idempotent set-union merges — read SYNC_PATTERN.md.

Scope (frozen)

Private database, one custom zone per client, fail-if-exists saves, change-token fetches, KV store, account events. That's the whole surface, on purpose, and it is not growing. No public database, no queries, no subscriptions/push, no CKShare, no CKSyncEngine, no Android (there is no CloudKit on Android). If you need those, fork freely — the code is small enough to actually read.

Why so small: this survey is what the CloudKit-for-React-Native landscape looked like when I needed it — one wrapper last touched in 2018, a couple of sub-five-star experiments, and one package with 207 commits in five weeks followed by silence. Maximal surfaces die. A small surface that runs in a shipped app can stay alive.

Requirements

  • iOS 15.1+ (raw CKDatabase async APIs; deliberately not CKSyncEngine, which would require iOS 17 and the remote-notifications entitlement)
  • Expo SDK 50+ (expo-modules-core), development build or EAS build (not Expo Go)

Install

npx expo install expo-cloudkit-bridge

Add the iCloud entitlements to your app.json (replace the container id with your own, and register the container in your Apple Developer account):

{
  "expo": {
    "ios": {
      "entitlements": {
        "com.apple.developer.icloud-services": ["CloudKit"],
        "com.apple.developer.icloud-container-identifiers": [
          "iCloud.com.example.myapp"
        ],
        "com.apple.developer.ubiquity-kvstore-identifier": "$(TeamIdentifierPrefix)$(CFBundleIdentifier)"
      }
    }
  }
}

Then rebuild your dev client (npx expo run:ios or an EAS build) — this is a native module; a Metro reload is not enough.

Quickstart

import {
  createCloudKitBridge,
  isCloudKitAvailable,
} from 'expo-cloudkit-bridge';

const ck = createCloudKitBridge({
  containerId: 'iCloud.com.example.myapp',
  // A custom zone is MANDATORY: the private DB's default zone cannot do
  // change-token delta fetches. Use a separate zone for dev builds so test
  // data never mixes with real data.
  zoneName: __DEV__ ? 'MySyncDev' : 'MySync',
  kvPrefix: 'myapp.settings.',
});

async function sync(myChangeToken: string | null) {
  if (!isCloudKitAvailable()) return; // OTA-safe on old binaries
  if ((await ck.getAccountStatus()) !== 'available') return;
  await ck.ensureZone();

  // Push: fail-if-exists → the first device to the server owns each key.
  const result = await ck.saveRecords([
    {
      recordName: 'entry:2026-07-19', // unique per ZONE across ALL types
      recordType: 'JournalEntry',
      fields: { text: 'hello', mood: 4, pinned: false },
    },
  ]);
  // result.saved    → keys this device now owns
  // result.conflicts → the SERVER's records; adopt them locally
  // result.errors   → retry later from your outbox

  // Pull: everything changed since your token (null = from the beginning).
  const { records, tokenB64 } = await ck.fetchChanges(myChangeToken);
  // ...merge records into your store, then persist tokenB64 yourself.
}

Settings-style preferences ride the iCloud key-value store (last-writer-wins, 1MB / 1024-key limits — use it for prefs, never for canonical data):

ck.kvSetItem('theme', JSON.stringify('dark'));

const unsubscribe = ck.onKvChange(({ reason, keys }) => {
  // Another device changed settings (or the initial cloud pull landed).
  const all = ck.kvGetAllItems();
  // ...apply to your local settings store.
});

API

| Member | What it does | | --- | --- | | isCloudKitAvailable() | false on binaries without the native module (OTA safety) and on non-Apple platforms. Gate every call site. | | createCloudKitBridge(config) | Returns a client bound to a container + zone (+ KV namespace). The native side stays stateless; multiple clients coexist. | | client.getAccountStatus() | available, noAccount, restricted, temporarilyUnavailable, couldNotDetermine. A fresh install's first check can transiently fail — retry with backoff and listen to onAccountChanged. | | client.getUserRecordId() | Opaque, stable per-iCloud-account id. Pin your sync pairing to it so an account switch pauses sync instead of mixing two users' data. | | client.ensureZone() | Idempotent create-or-confirm. Surfaces per-zone failures the raw API hides. | | client.saveRecords(records) | Batch save with .ifServerRecordUnchanged (fail-if-exists), non-atomic so one conflict never fails the batch. Chunk to ≤250. Conflicts return the server's record. | | client.fetchChanges(tokenB64) | Delta fetch since the token; loops moreComing internally. Persist the returned token yourself. | | client.kvSetItem/kvGetItem/kvRemoveItem/kvGetAllItems | Namespaced iCloud key-value store access. Values are JSON strings; JS owns the schema. | | client.onKvChange(fn) | External KV changes, keys filtered to your namespace. The initialSync event is unreliable on real devices — also re-apply kvGetAllItems() on every app foreground. | | client.onAccountChanged(fn) | iCloud sign-in/out/switch. | | client.deleteRecords / deleteZone | Destructive; meant for dev/test tooling. Gate callers in release builds if your design is append-only. |

Errors reject with stable codes on err.code: CK_NOT_AUTHENTICATED, CK_ZONE_NOT_FOUND, CK_TOKEN_EXPIRED (re-fetch from null), CK_QUOTA_EXCEEDED, CK_RETRY_LATER, CK_NETWORK, CK_CONFLICT, CK_BAD_INPUT, CK_ERROR.

Field notes (the potholes, so you don't hit them)

Hard-won on the way to production; the long versions live in SYNC_PATTERN.md:

  • Record names are unique per zone across ALL record types. Two types sharing a natural key silently lose every save to a conflict. Namespace.
  • Deploy your schema to Production. Just-in-time record-type creation only works in CloudKit's Development environment. Simulator/Xcode builds hit Development; TestFlight, App Store, and EAS ad-hoc builds pin to Production. Forget the console's "Deploy Schema Changes" and store users get silent nothing.
  • codesign lies about simulator builds. Sim builds carry entitlements in a linker section, not the signature — an empty codesign -d --entitlements readout does not mean the build is broken (and "fixing" it by signing restricted entitlements makes the app unlaunchable).
  • Container propagation takes time. A fresh App ID ↔ container association can return "Invalid bundle ID for container" for the better part of an hour with a fully correct client. Verify your side once, then wait.
  • A fresh install's first account check races. couldNotDetermine on launch is transient; retry with backoff or sync stays dead until the next foreground.
  • Verify the artifact. After any build-system change, unzip the ipa and strings the binary for your module's name before shipping. This module once shipped missing from a production build because of an unanchored ios/ gitignore pattern in the parent project.

License

MIT © Jason Stiles