Archive for April 1, 2026

Wednesday, April 1, 2026

CloudKit Problems With iOS 26.4

Lukas Kubanek:

Looks like Apple broke CloudKit sync in OS 26.4. Remote notifications don’t seem to arrive, so no updates unless the app is relaunched.

Sean Heber:

There are so many annoying limits and throttles when dealing with iCloud/CloudKit. Now I ran into one where the subscriptions that let you know when content changed are also throttled and limited. From what I’m reading, I might be in push-notification-jail for 24 hours now because I triggered a ton of changes during a test.

Makes it a tad hard to know when I broke something vs. when iCloud just decides to stop sending me stuff for a while.

Sean Heber:

Has anyone had a CKSubscription just… stop? It’s a zone change subscription. It’s supposed to generate an invisible push so I can refresh things. This used to work. Then early yesterday it didn’t work anymore. I’ve tried everything I can think of including reverting all code entirely back to a state from a few days ago when I know it worked. But it still doesn’t work. I’ve rebooted things. I’ve reinstalled things. It’s been 27ish hours now since I last saw it work.

Steve Troughton-Smith:

Looks like the CloudKit sync issue in *OS 26.4 is real (I can repro with all of my apps), and has the very distinct potential to lead to catastrophic data loss and/or sync conflicts across effectively all apps. Apps only receive changes from the cloud after being quit and relaunched.

Amy Worrall:

Anyone know if the iOS 26.5 beta fixes the CloudKit subscriptions not working bug from 26.4?

Ged Maheux:

It’s hilarious that the iOS 26.5 beta release notes make no mention that they fixed this massive regression with iCloud push sync Apple broke in 26.4.

It seems serious enough to warrant a 26.4.1 update.

Previously:

CKSyncEngine

WWDC 2023:

Discover how CKSyncEngine can help you sync people’s CloudKit data to iCloud. Learn how you can reduce the amount of code in your app when you let the system handle scheduling for your sync operations. We’ll share how you can automatically benefit from enhanced performance as CloudKit evolves, explore testing for your sync implementation, and more.

The documentation:

The sync engine uses an opaque type to track its internal state, and it’s your responsibility to persist that state to disk and make it available across app launches so the engine can function properly.

For example, if you delete an object while offline, you can send that change to the engine and let it keep track of whether it’s been pushed to the server. In theory, you don’t have to manage tombstones yourself as long as you persist the engine’s state. When you add or change objects, the state stores just the IDs so it shouldn’t grow too huge.

There’s some sample code, but it’s frustrating in that the conflict resolution is rather basic. I think you actually can access the ancestorRecord to do a three-way merge, but they don’t show that.

Sean Heber (previously):

I removed CKSyncEngine and did it all myself solving the problems we needed solved. Maintaining a synced database of items with unique IDs supporting cascade deletes and automatically handling conflicts without much participation from the server which knows nothing of our needs.

[…]

One of the key problems we had was how CloudKit replays deletion tombstones going way back. Tapestry would download feed items, make a CKRecord, and put it in iCloud to sync to other devices. Then later when the item gets old, we delete them. Over time, with busy feeds, you’re looking at hundreds of expiring items per day for some people.

[…]

Those replays are a total waste of time but it’s just how it works. It could take hours to restore the relatively small number of actually current records as CloudKit replays dead records the new install never had in the first place. Thousands and thousands of them.

I had to rearchitect everything to redesign it around this one unchangeable behavior.

You could probably fetch the initial data manually, using CloudKit directly, before initializing CKSyncEngine. Then it would take a while for it to catch up, but at least the app would be usable. It seems like the engine should just handle this better, though.

Christian Selig (Mastodon, tweet):

I’ve had a lot of fun working with CKSyncEngine over the last month or so. I truly think it’s one of the best APIs Apple has built, and they’ve managed to take a very complex topic (cloud syncing) and make it very digestible and easy to integrate, without having to get into the weeds of CKOperation and whatnot like you had to in previous years.

More interesting for a blog post, perhaps, I also had a fair few questions going into it (having very little CloudKit knowledge prior to this), and I thought I’d document those questions and the corresponding answers, as well as general insights I found to potentially save a future CKSyncEngine user some time, as I really couldn’t find easy answers to these anywhere (nor did modern LLMs have any idea).

[…]

But you should not have multiple CKSyncEngine instances managing a single private database (I naively tried to do this to have a nice separation of concerns between different types of data in the app). The instances trip over each othre very quickly, with it not being clear which instance receives the sync events.

[…]

In this [the case of quotaExceeded] Apple pauses the queue until the user frees up space or buys more (or after several minutes, specified by retryAfterSeconds) but does not add your item back, which seems weird to me, so just add it back. But you also can’t just add it back, as that would put it at the end of the queue, so you have to insert it back at the beginning of the queue so it’s the next item that will be retried (since it just failed). Only, there’s no API for this, so grab all the items in the queue, then empty the queue, then re-add all items back to the queue with your failed item at the front.

Stéphane Lizeray:

CKSyncEngine has greatly simplified the integration with CloudKit and the error handling though. It was way more difficult before, I would say almost impossible before.

[…]

There are several issues here. The first one is not related to CKSyncEngine but to the replay API of CloudKit. The second one is that you have options when fetching changes to fix this issue (Scope + prioritizedZoneIds) but it is supposed you designed your schema accordingly. And it didn’t exist with CKFetchRecordZoneChangesOperation only with CKSyncEngine… The third issue is that you can’t use ZoneOptions.desiredKeys

See also: Harmony, which uses CKSyncEngine on GRDB (via Aaron Pearce).

Previously:

axios Compromised on NPM

Ashish Kurmi (Hacker News):

axios is the most popular JavaScript HTTP client library with over 100 million weekly downloads. On March 30, 2026, StepSecurity identified two malicious versions of the widely used axios HTTP client library published to npm: axios@1.14.1 and axios@0.30.4. The malicious versions inject a new dependency, plain-crypto-js@4.2.1, which is never imported anywhere in the axios source code. Its sole purpose is to execute a postinstall script that acts as a cross platform remote access trojan (RAT) dropper, targeting macOS, Windows, and Linux. The dropper contacts a live command and control server and delivers platform specific second stage payloads. After execution, the malware deletes itself and replaces its own package.json with a clean version to evade forensic detection.

Carly Page:

The releases didn’t come through the project’s usual build process either. Security firm StepSecurity found that both versions were published via the compromised npm account of “jasonsaayman,” the project’s primary maintainer, who was reportedly locked out of the account while the packages were being pushed.

The attackers swapped the account’s email address for an anonymous ProtonMail inbox and pushed the infected packages manually via the npm CLI, completely bypassing the project’s GitHub Actions CI/CD pipeline and the safeguards developers tend to assume are in place.

Previously: