Tomas Vik

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.

Theme illustration

“Illustration” by Ljubica Petkovic

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.

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:

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.