What One Deleted Account Taught Us About Our iPhone Sync

By James Florence, founder of SoKal · October 23, 2026

A white sign reading Delete fixed to a graffiti-covered wall, with the SoKal logo in the corner.
One new user tapped Delete account 19 minutes after signing up. Photo by Marija Zaric on Unsplash.

A new user deleted their account 19 minutes after signing up. When we asked why they were leaving, they picked "too slow or buggy."

My first reaction was simple. It's not slow, so it's buggy. But which bug? My gut said a recurring event was landing on the wrong date. I was half right, and the half I got wrong turned out to matter more.

This post walks through how we reconstructed what that person did without knowing who they were, what we found in the code behind it, and what we changed. One user's 19 minutes led us to a sync bug that affected every iPhone user with repeating events.

Two systems that deliberately don't talk to each other

SoKal is a social calendar. You add events or connect the calendars you already use, and friends see what you choose to share. Privacy works by event type: travel, outing, exercise, online, errand, visitor, work, blocked, or no type at all. Anything you don't share shows to friends as a grey "Busy" block.

Because the product is about sharing, we are strict about what we collect. Two tools made this investigation possible, and we keep them apart on purpose.

The "Why are you leaving?" answer

We ask this during account deletion. The answer travels inside the delete request and lands in its own anonymous table. Free text is scrubbed before it is stored: handles, links, long runs of digits, and the person's own name, username and email are all replaced with [removed]. Our privacy policy promises the answer is kept with nothing that links it to you.

The deletion timeline email

When someone permanently deletes their own account, the backend emails us a timeline of what they did in the app over the previous 24 hours. Nothing is stored. It exists only as that one email. The rules it follows:

The data underneath is our own analytics table. Each row is an action name, a platform, an entity type and a little metadata. Event titles are never logged. That has been a standing rule since day one.

How the two got matched anyway

I want to be straight about this part. No system links the leaving answer to the timeline. But we are small. Only five leaving answers had ever been recorded. When the timeline email arrived, I opened the leaving reasons dashboard and saw the count go from five to six, with our first ever "too slow or buggy" answer.

Timing lined them up, not code. That is a real privacy nuance at small scale, and it's worth naming. The safeguards still hold where they count: the answer carries no identity, and the timeline never names the person. Even with both in front of me, I have no idea who this was. I only know what they tried to do.

What 19 minutes looked like

A new user in Europe, using the app in Polish, on an iPhone. Here is what the timeline showed.

Nothing in there says "crash." Nothing mentions recurrence or wrong dates. What stands out is a loop: change a setting, check the result, change it again.

How we investigated

We read the timeline first and formed hypotheses second. Then we split the work into three read-only questions and ran them in parallel as separate code investigations, using Claude Code sub-agents:

  1. How does Apple Calendar sync handle edits, deletes and repeating events?
  2. What exactly does each analytics field mean? Some of our own log fields were ambiguous.
  3. What does the "View as connection" preview actually render, and does it match what friends really see?

We treated every report as a lead, not a fact, and verified each headline claim in the code before acting on it. That mattered, because we misread our own logs three times.

What we found

Problem 1: repeating iPhone events collapsed into one event, on a random date

This one is confirmed in code and affected every iPhone user with repeating events. We can't prove from the log that this user hit it, since sync left no trace at the time. But most real calendars have repeats in them, and theirs had 171 events.

Apple's EventKit gives every occurrence of a repeating event the same eventIdentifier. Apple's own docs say to use the start date to tell occurrences apart. We keyed synced events on that identifier alone, so a weekly meeting became one SoKal event. It landed on whichever occurrence was processed last, often months away.

It got worse the deeper we looked:

Problem 2: deleting imported events was inconsistent and silent

Problem 3: the friend preview could never change

This is the most likely trigger. Only travel, outing, exercise, online, errand and visitor could be shared. Untyped, work and blocked events always showed as Busy, no matter what.

Every one of this user's events was untyped, because that is how imported events arrive. So every sharing switch they flipped left the preview exactly the same. Four rounds of changing settings and checking, and nothing moved. Nothing on screen explained why.

The wording didn't help. The event form said those events were "Not visible to friends," while the preview showed them as visible Busy blocks. Both were half true. Side by side, they read as a contradiction.

The preview had its own gap too. It ignored two settings that friends' real view respects: an event's own "hide from" list, and the owner's choice of showing hidden events as Busy or not at all. So the preview could differ from what friends actually saw.

Our reading, and it is an inference: this person was trying to control what friends would see, and the controls appeared to do nothing. That fits "buggy" better than any crash would. A lot of the time, "buggy" means "I did something and nothing changed."

What we fixed

Repeating events

Each occurrence of a repeating event now gets its own key. One-off events keep their old key, so nobody's existing events get rebuilt.

if (isRepeating(raw) && raw.startDate) return `${raw.eventIdentifier}::${raw.startDate}`;
return raw.eventIdentifier;

One known limit: changing a whole series on your iPhone rebuilds its occurrences in SoKal. The clean fix needs our native code to send each occurrence's original date, which means a new App Store build. The key format already has room for it.

A real choice when deleting

Deleting an event that came from Apple Calendar now asks whether to remove it from SoKal only, which is the default, or delete it from Apple Calendar too. The choice appears on every delete path: phone, tablet, foldable and the details screen. Events you created in SoKal still delete their phone copy, as before.

Sharing that actually responds

Work and untyped events can now be shared. Both are off by default for everyone. Blocked can never be shared.

Event typeCan be sharedWhere friends see it
Travel, outing, exercise, online, errand, visitorYesCalendar, feed, notifications
Work, Other (no type)Yes, off by defaultCalendar only
BlockedNeverBusy block only

Calendar-only means friends see the event on your calendar, but it never enters the feed, notifications or alerts. We enforce that in SQL on every feed query, with a test that proves it:

array_remove(array_remove(COALESCE(<approved types>), 'default'), 'work')

The confusing label is gone too. Unshared events now say "Friends see you're busy, not what it is." The preview now follows the same rules as the server, including per-event hiding. All new text ships in all nine languages on app and web.

Logging we added

Answering "what was this person doing?" took inference in places where better logs would have given us facts. So we closed the gaps:

Still no titles. The leaving answer still never goes into the timeline.

What we took away

Privacy and debuggability aren't opposites. Code-shaped metadata, no ids, no titles, local times, and an email that is never stored. That was enough to reconstruct what someone was trying to do.

Read the code behind what the user touched. The log can't prove this person hit the repeating-event bug. We found it anyway, because we followed their path into the sync code.

Dead controls feel like bugs. The likely trigger wasn't a crash. It was a switch that looked like it did nothing.

Check the exact path. Two screens doing the same job differently almost gave us the wrong answer.

Log for the next investigation, not just the dashboard.

The part that still bothers me is everything before this. As I put it to the team: "I don't know how many users we've lost because of that, and people just didn't answer. Or we didn't have logs in place." This person left us a reason and a trail. Most people who leave don't. Building so that the ones who do can teach you something is worth it.

SoKal is a social calendar for seeing what your friends are up to and making plans when you're all free. It's free on iPhone, iPad, Mac, Android and the web.

Download SoKal on the App Store Get SoKal on Google Play

Read next