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.
Potential solution
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.
Overview
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.
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:
timezone
:
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
event-20201101T142809Z
:
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
todo-20201101T144314Z
:
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 event
and todo
files each day.
Synchronisation
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.
Calendar clients like Thunderbird for desktop and Etar on Android) would connect to the local server.
With some help, I could implement a proof of concept of such a local-first calendar in a week or two. Please let me know if you’d be interested in cooperating on implementing this in Go or JavaScript/TypeScript. My email is me@viktomas.com. Also please let me know if I’m reinventing the wheel and someone has already done something similar.