I want a local-first calendar
I’m at the peak of my degoogling, slowly migrating of Gmail. I still use Google calendar, but hopefully, there’s an alternative. My personal calendar is much less collaborative than my work calendar. I don’t share it with friends, and I don’t use it to organise meetings. That makes it a perfect candidate for a local-first app. Storing my calendar on my devices and synchronising it through services like Dropbox or git.
Note: this article assumes an understanding of
git version control system.
The only local, multi-platform calendar that I know of is Thunderbird. It allows users to export their whole calendar in the iCalendar
.ics file. The textual iCalendar format could be good enough if there was a way to synchronise it between multiple devices. My use case is having one calendar on both mobile phone and desktop and synchronise it over Dropbox or similar cloud storage provider.
In this regard, I love Joplin, a local-first, open-source note-taking app that has both desktop and mobile clients synchronised with Dropbox.
After thinking about this problem for a while, I came up with a solution for a local-first calendar. The solution has to support one-person calendar needs. Advanced scheduling is out of scope. The more we can utilise current protocols and clients, the better. I have no intention of implementing a Thunderbird equivalent.
A large number of calendar clients supports connecting to CalDAV servers1. And so that’s where the implementation would start. My idea is that the custom CalDAV server would run locally on the device.
The CalDAV server would be a thin layer on top of a partitioned iCalendar format (explained below).
Finally, the whole calendar data structure would be saved and synchronised in git.
graph TD; A["Generic calendar client"] --> B[local CalDAV server] B --> C[Partitioned iCalendar format] C --> D[git]
Data format - partitioned iCalendar
Let’s start with the universal standard; the underlying structure would be partitioned iCalendar. The full calendar is a text file looking like this:
BEGIN:VCALENDAR PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN VERSION:2.0 BEGIN:VTIMEZONE TZID:Europe/Prague BEGIN:DAYLIGHT .. END:DAYLIGHT BEGIN:STANDARD .. END:STANDARD END:VTIMEZONE BEGIN:VEVENT CREATED:20201101T142809Z DTSTAMP:20201101T144346Z SUMMARY:Test DTSTART;TZID=Europe/Prague:20201101T073000 DTEND;TZID=Europe/Prague:20201101T083000 DESCRIPTION:Event description BEGIN:VALARM .. END:VALARM END:VEVENT BEGIN:VTODO CREATED:20201101T144314Z DTSTAMP:20201101T144314Z SUMMARY:Don't forget this todo END:VTODO END:VCALENDAR
(I added indentation for easier reading.)
We would split this file into several smaller files:
BEGIN:VTIMEZONE TZID:Europe/Prague BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19700329T020000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19701025T030000 RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 END:STANDARD END:VTIMEZONE
BEGIN:VEVENT CREATED:20201101T142809Z DTSTAMP:20201101T144346Z SUMMARY:Test DTSTART;TZID=Europe/Prague:20201101T073000 DTEND;TZID=Europe/Prague:20201101T083000 DESCRIPTION:Event description BEGIN:VALARM ACTION:DISPLAY TRIGGER;VALUE=DURATION:-PT15M DESCRIPTION:Default Description END:VALARM END:VEVENT
BEGIN:VTODO CREATED:20201101T144314Z DTSTAMP:20201101T144314Z SUMMARY:Don't forget this todo END:VTODO
Splitting the events, todos, and other entries into separate files would allow us to keep the vast majority of the files unchanged during day to day use. In other words, instead of editing the large root iCalendar file all the time, we only need to edit a handful of small
todo files each day.
Now that we’ve got the whole calendar divided into small text files, we can choose the best available tool for synchronising text files:
git. Each edit to our calendar (e.g. changing and saving an event) is going to trigger a git commit.
We will synchronise the calendar across devices by pushing to our central repository from one device and pulling from the other. Synchronisation (Merge) conflicts shouldn’t happen too often, but in the first iteration, we can resolve them using plain
git merge logic.
The cool thing about using Git is that someone can maintain their personal (or team) calendar in git with Merge Requests. The team can, for example, discuss when would they like to have their stand up before merging the event.
Clients connect to CalDAV server
Implementing and running the server seems to be the trickiest part of the implementation. Maybe it would be possible to replace the internals of Fennel.js to connect to the partitioned iCalendar data structure. Another tricky part would be to make that server run on mobile devices. But I have seen it done before. For example, the Transmission BTC starts an HTTP server.