All posts

Two-way calendar sync that keeps the edit you have not sent yet

Two-way sync with Google or Outlook has one classic failure: a sync that reads first and quietly undoes the meeting you moved a minute ago. Here is the order Nemi Calendar syncs in, the second rule behind it, the one case where we let your version win on purpose, and a repeating event you can take apart.

Engineering · · 8 min read

Two-way calendar sync sounds simple: send your changes to Google, fetch Google's changes back. The trouble is the order. Do those two things the wrong way round and a sync can quietly undo the meeting you moved a minute ago, and nobody, including you, would be told.

This post is about how Nemi Calendar keeps a change you have made but not yet sent, why a repeating event is stored as one row and not as a hundred, and the two places where the rules deliberately let the other side win.

How can a calendar sync lose an edit?

Picture a meeting at 10:00 that you drag to 14:00 in Nemi. For a moment the two calendars disagree: Nemi says 14:00 and Google still says 10:00. Now a sync runs. If it reads from Google first, it finds a meeting at 10:00, sees that Nemi's copy is different, and brings it “up to date”. Your move is gone, and when the sync then sends local changes out, there are none left to send.

Nothing about that looks like an error. Every step did what it was written to do. It is the kind of bug that shows up as a client turning up at the wrong time, which is why Nemi's sync is built so it cannot happen. It is the same problem a document editor has with an edit it has not saved yet, with a second party that can change things too. Step through it in the three orders below.

One meeting, one sync, three orders

NemiDesign reviewTue 10:00GoogleDesign reviewTue 10:00

1/4Both calendars agree

Design review, Tuesday at 10:00, in Nemi and in Google.

Pull first is drawn only to show the failure. Nemi pushes first, and if a push fails, a second rule keeps the pull from overwriting your change.

Why does Nemi push before it pulls?

Every change you make in Nemi is saved straight away and marked as not yet sent. A sync of a Google or Outlook account then does two things, always in this order: it sends every marked change out to the provider, and only then reads what the provider has. By the time the read happens, your change is already on the other side, so the read brings back your own version.

The mark is cleared in exactly one place: when the provider has accepted the change. Not when the request was sent, not when the sync finished, only on success. If the push fails, because the provider refused it for a moment or the request never arrived, the change stays marked and is sent again on the next sync. You keep seeing your own version in the meantime, because it is the true one.

The not-yet-sent mark is cleared in exactly one place: when the provider says yes.

Then there is a second rule, for the case the first one cannot cover. When a pull brings in an event that is still marked locally, it refuses to write over it and skips it. So even when a push failed and the read that follows brings back the old time, your change survives until it can be sent. The one thing a pull applies anyway is a deletion or a cancellation made on the other side: a meeting somebody removed is removed here too. The two rules overlap on purpose: either one alone would protect you most of the time, and together they protect you in the order that actually happens.

Is there anything this does not protect?

Yes, and it is a choice rather than an oversight.

Two edits to the same event at once: yours is sent last. Nemi does not ask the provider “has this changed since I last saw it?” before pushing. If you and somebody in Google both move the same meeting before a sync runs, your push goes out first and replaces their version, and the read that follows brings back yours. That is a real trade: the alternative is to stop and ask which version to keep, which is the right answer for documents and the wrong one for a meeting time, where the last deliberate move is usually the one that counts.

What is protected

A change you made that has not reached Google or Outlook yet: a pull never writes an older version over it, whatever order the reads and failures happen in.

What is not, by design

Somebody else's edit to the same event, made in Google or Outlook in the same few minutes as yours. Your push goes out first, so your version is the one that stays. The one thing your edit never undoes is a delete: an event removed in Google stays removed, and the next sync takes it out of Nemi too.

How are repeating events stored?

A weekly stand-up that never ends has infinitely many occurrences, so storing each one is not an option. Nemi stores the series the way Google and Outlook describe it: one row holding the first occurrence and a rule, written in the standard calendar format, such as FREQ=WEEKLY;BYDAY=TU. The occurrences are worked out from the rule whenever a week is drawn, and never written down.

Changing one occurrence adds one row, an override, that points at the series and says which occurrence it replaces. Moving next Tuesday's stand-up to 11:00 is an override with the new time. Cancelling it is an override marked cancelled, which leaves a hole in the series, exactly as Google and Outlook express “I deleted this one”. Try it: pick an occurrence and move or cancel it.

One series, expanded by the calendar's own code

MonTueWedThuFriSatSun
5 Oct
6 Oct
7 Oct
8 Oct
9 Oct
10 Oct
11 Oct
12 Oct
13 Oct
14 Oct
15 Oct
16 Oct
17 Oct
18 Oct
19 Oct
20 Oct
21 Oct
22 Oct
23 Oct
24 Oct
25 Oct, clocks go back
26 Oct
27 Oct
28 Oct
29 Oct
30 Oct
31 Oct
1 Nov
2 Nov
3 Nov
4 Nov
5 Nov
6 Nov
7 Nov
8 Nov
9 Nov
10 Nov
11 Nov
12 Nov
13 Nov
14 Nov
15 Nov
16 Nov
17 Nov
18 Nov
19 Nov
20 Nov
21 Nov
22 Nov
23 Nov
24 Nov
25 Nov
26 Nov
27 Nov
28 Nov
29 Nov

Pick an occurrence to move or cancel just that one. Note the time stays at 09:00 on both sides of 25 October, while the stored time in UTC moves by an hour.

What is stored

  • 1 series: Every week on Tue, from 6 Oct at 09:00

What is drawn, in these eight weeks

8

occurrences from 1 stored row

Everything drawn here is the output of expandEvents and describeRRule, the functions Nemi Calendar runs, on a series that starts on 6 October 2026 at 09:00 in Amsterdam. Summer time ends on 25 October.

Why does the meeting stay at 09:00 when the clocks change?

Every time in Nemi is stored as an exact instant in UTC, so a row means the same moment to everybody who reads it. But a repeating meeting is not a fixed number of hours apart. A weekly stand-up at 09:00 in Amsterdam is 07:00 UTC in October and 08:00 UTC in November, because the clocks go back in between. A rule expanded by adding exactly seven days of milliseconds would put it at 08:00 local time for the whole winter.

So expansion happens on the wall clock of the event's own time zone: the rule produces “Tuesday, 09:00, Amsterdam”, and only then is that turned into an instant. That is why the explorer shows 09:00 on both sides of 25 October while the stored UTC time moves by an hour. That holds for every series, wherever it was made. Outlook hands its times over in UTC, so for a series from Outlook Nemi takes the organiser's own time zone from the event and expands it on that wall clock, the same way.

The expander handles the rules Google's own editor can produce, including “the last Friday of the month” and series that stop after a number of times or on a date. A Google rule with parts it does not use is stored exactly as it arrived and expanded on the parts it understands, so nothing in it is silently dropped. Outlook describes a repeat in its own format, which Nemi translates into a rule; a pattern that cannot be translated becomes a single event rather than a wrong series.

Who can see a connected calendar?

A connected calendar belongs to your account, not to a workspace, so connecting one shows it to nobody else in the workspaces you are in. Nemi asks Google for your name and email address, a read only list of your calendars, and access to calendar events, and it only reads the events in the calendars you enable. The access it is given is encrypted before it is stored; what encrypted at rest means, and what it does not protect, has a post of its own. ICS subscriptions are read only and are never written to. The details are in section 3.7 of the privacy policy.

For connecting an account step by step, see Connecting Google, Outlook and ICS feeds; for repeats and the “this event only” choices, see Events, invitations and repeats. The rest of the app is on the Calendar page.

Questions people ask

Does Nemi Calendar sync both ways with Google Calendar?

Yes. A change you make in Nemi is sent to Google, and changes made in Google are read back into Nemi. Nemi always sends its own changes before it reads, so a sync never brings back the old version of something you just moved.

Can I sync my Outlook calendar with Nemi?

Yes, Microsoft 365 and Outlook.com accounts connect the same way and sync in both directions, with the same push before pull rule. Outlook describes a repeat in its own format, which Nemi translates; a pattern it cannot translate shows as a single event rather than a wrong series.

Why did my calendar sync undo my change?

Usually because the sync read from the provider before it sent your local change, saw the old version there and copied it back over yours. Nothing reports an error, because every step did what it was written to do. Nemi sends its changes first and never lets a read overwrite a change that has not been sent yet; the one thing a read still applies is an event deleted or cancelled on the other side.

How often does Nemi Calendar sync?

When you move a meeting, Nemi sends that change within seconds. Opening Calendar syncs any account that has not synced in the last 4 minutes, and a background job every 5 minutes picks up accounts that have gone 15 minutes without a sync. The refresh button syncs straight away.

What happens if two people edit the same event at the same time?

In Nemi, the last change sent wins. If you and somebody in Google both move the same meeting before a sync runs, your change goes out first and replaces theirs. For a meeting time, the last deliberate move is usually the one that counts, so Nemi does not stop to ask.

Can people in my workspace see my connected calendar?

No. A calendar you connect to Nemi belongs to your account, not to a workspace, so nobody else in your workspaces sees it. ICS subscriptions are read only and Nemi never writes to them.

What access does Nemi ask Google for?

Your name and email address, a read only list of your calendars, and access to calendar events so that changes made in Nemi can reach Google. Nemi only reads events in the calendars you enable, and the access Google gives it is encrypted before it is stored.

Where the numbers come from

  • Push before pull, and when a sync runs: lib/calendar/sync.ts (syncAccount, pushEventNow, syncAllDueAccounts, SYNC_STALE_MS)
  • A pull never writes over an unsent change, but does apply a deletion: lib/calendar/providers/upsert.ts (upsertRemoteEvents)
  • The mark is cleared only on a successful push: lib/calendar/providers/google.ts, lib/calendar/providers/microsoft.ts
  • Series, overrides and expansion in the event's zone: lib/calendar/occurrences.ts (expandEvents), lib/calendar/rrule.ts, lib/calendar/time.ts
  • The background schedule: lib/server-init.ts
  • What a connected calendar shares, section 3.7: /privacy
  • The Google scopes and the Outlook translation: lib/calendar/providers/google.ts (SCOPES), lib/calendar/providers/microsoft.ts (toRRule)

Read next

Use the thing we write about.

Files, docs, sheets, photos, calendar and meetings in one account.