← All notes

Work from

When sync succeeds but nothing is saved

A completed CloudKit operation wasn’t proof that the record was saved.

Two phones could belong to the same Radish household while their receipt histories stopped converging. The problem appeared as the history grew, even though the application could report a successful upload.

There were two separate failures: the snapshot no longer fit its transport, and the success check did not prove that CloudKit had saved the record.

Account for the stored representation

Receipts already travelled as a CloudKit asset. Auxiliary state, including aliases and classification marks, remained inline in the member record. Those fields grew with use too.

Sealing and serialization expanded the data. In this format, nested binary values passed through multiple base64 encodings. Comparing the local files’ sizes with the record limit understated what would actually be sent.

A reproduction using synthetic bytes at the observed lengths exceeded the inline record limit without reading or exposing receipt contents. Because the components were saved together, the auxiliary fields could block publication of new receipts as well.

The repair moved the growing encrypted components into assets within a versioned snapshot transport. It preserved the sealed data instead of truncating history to make the upload fit.

Confirm the individual write

CloudKit’s modifyRecords returns results for individual records. Completing the outer operation is not enough: the expected record must have a successful save result. A missing result is not success either.

The publication path now checks that result before confirming the upload. A persistent sync journal tracks the local revision separately from the confirmed revision, so pending work survives a restart. An empty pull means that no new remote changes were returned; it cannot clear an unresolved upload failure.

That distinction also matters during concurrent edits. Confirming one snapshot must not mark a newer local change as uploaded.

Keep the repair’s limits visible

Regression coverage includes per-record failures, missing save results, persistent pending state, and large synthetic histories. Moving data into assets solves the inline size constraint; memory use, merge cost, and device behavior still need their own checks.

The new transport also requires compatible app versions on both phones. Migration and recovery must preserve the existing history; a downgrade is not a substitute for a compatible repair.

The success condition is specific: the intended record was saved, and the application knows which local revision that save represents.

From work on Radish. The application repository is private; the size reproduction described here uses synthetic data.