I've tried quite a few productivity tools over the years, and at some point I started feeling like I was spending more time managing the tools than actually using them.
My calendar was in Google Calendar. Tasks were somewhere else. Notes related to those events were in another app.
And when I tried using more powerful tools to keep everything in one place, I ended up having to learn their own system — databases, properties, templates, views, and so on.
I wanted something simpler.
So I started building PlanMini, a personal planner that brings schedules, tasks, and notes together and lets them reference each other.
The goal was pretty simple: open it and use it.
No new productivity methodology to learn, and no complicated workspace to set up.
I didn't want to run a traditional backend
One of the decisions I made early on was that I didn't want PlanMini to store everyone's personal data on a backend I had to operate myself.
I wanted this to stay a lightweight service.
Running user accounts, databases, backups, server infrastructure, and dealing with failures felt like a lot of responsibility for what was supposed to be a small personal productivity app.
But making it local-only wasn't really an option either.
I wanted to write something at work and see it again at home. If I changed an event in the web app, the desktop app should eventually have the same data.
So the data had to live somewhere.
Then I realized I was already using two services that fit the problem pretty well:
Google Calendar and Google Drive.
Calendar events could stay in Google Calendar, while notes and other PlanMini data could live in Google Drive.
Instead of trying to replace Google's ecosystem, PlanMini could sit on top of it and provide a simpler way to work with schedules, notes, and tasks together.
At first, I thought this would make the architecture much simpler.
In some ways, it did.
I didn't have to run my own database for user data.
But it created another problem.
Synchronization.
Not having a backend doesn't make sync easy
Just because data is stored in the cloud doesn't mean every device always has the same version of it.
The web app can be open while the desktop app is changing the same data. Something edited at work can be changed again later at home.
At first, I thought sync would mostly be:
Download the latest data, make a change, upload it again.
That works until two clients start changing the same thing.
For example, the desktop app might change the title of an event while another client changes its location.
If one side simply overwrites the other, one of those changes disappears.
It gets worse when both clients change the same field to different values.
Using the latest updatedAt timestamp isn't enough either. The most recent write isn't necessarily the version the user actually wants to keep.
So I ended up handling conflicts differently depending on the type of data.
Calendar events use ETags and a three-way comparison
Google Calendar gives each event an ETag.
PlanMini treats that roughly like a version identifier.
When updating an event, the app sends the ETag it originally read using If-Match.
If another device changed that event in the meantime, Google Calendar responds with:
412 Precondition Failed
At that point, PlanMini doesn't simply retry the write.
It compares three versions:
-
base— the event from the last successful sync -
local— the version changed on the current device -
remote— the latest version from Google Calendar
The comparison happens field by field.
If the desktop app changes the title while another client changes the location, both changes can be merged automatically.
If both sides change the title to different values, that's treated as a real conflict and the user has to choose which one to keep.
If both sides change the same field to the same value, there's nothing to resolve.
Some changes, such as concurrent edits involving recurrence rules, are handled more conservatively and stop automatic merging.
One thing I learned here was that concurrent changes and conflicts aren't exactly the same thing.
Two clients can edit the same event at the same time without actually touching the same information.
I also wanted notes to stay close to the schedule
A calendar event usually has more context than just a date and time.
A meeting has notes. A photo shoot might have a checklist. A project milestone can have related material.
So PlanMini lets me link notes directly to events.
The event itself still lives in Google Calendar, while PlanMini manages the note data separately.
From the user's point of view, though, I wanted it to feel like one workflow.
That meant notes needed their own synchronization strategy.
Notes don't use "latest version wins"
Every note change creates an operation.
An operation contains information such as:
-
operationId— a unique ID for the change -
parentOperationIds— which previous operation this edit was based on -
deviceId— which device created it -
payloadSha256— a hash used to verify the content
A normal edit history looks like this:
A → B → C
B was created after seeing A, and C was created after seeing B.
No problem.
But imagine the desktop app and web app both read A, then make different edits before seeing each other's changes.
Now the history looks like this:
B
/
A
\
C
B was created without knowing about C, and C was created without knowing about B.
PlanMini treats that as a branch in the history rather than simply asking which operation has the later timestamp.
Both versions are preserved.
When the user resolves the conflict, PlanMini creates a new resolve operation that references both branches:
B ──┐
/ │
A ├── Resolve
\ │
C ──┘
That resolution can then sync to the other devices, so they eventually converge on the same result.
I don't try to merge note text sentence by sentence.
If PlanMini can't know what the user intended, I'd rather preserve both versions than guess and silently destroy part of a note.
Tasks and settings use the same operation-history engine, although tasks are currently synchronized as a whole collection rather than merged item by item.
Backup recovery had the same kind of problem
Once sync was working, I ran into a similar issue with backups.
At first, restoring a backup sounds straightforward:
Load the backup and replace the current data.
But that can be dangerous.
The backup might be old, while the current data contains newer changes that are still perfectly valid.
So PlanMini doesn't restore a backup by replacing everything.
It compares the backup with the current data item by item.
If something exists only in the backup, it can be restored.
If the actual content is identical, the current version stays as it is.
If the same item exists in both places but contains different data, PlanMini doesn't choose automatically.
It shows both versions and asks the user which one to keep.
The comparison also ignores metadata that shouldn't decide whether two items are meaningfully different, such as updatedAt, revision values, or remote file IDs.
Dates are normalized before comparison.
Attachments are compared using SHA-256 hashes.
That means an older backup can still be restored if the user explicitly chooses it, while two items with different timestamps but identical content aren't treated as different for no reason.
What if the data changes while the recovery screen is open?
This was another edge case that turned out to matter.
Suppose PlanMini compares the backup with the current data and shows the recovery preview.
The user leaves that window open for a while.
During that time, something else changes the current data.
If PlanMini simply applies the old recovery decision later, it's applying a decision based on a state that no longer exists.
So when the recovery preview is created, PlanMini also creates a fingerprint of the relevant data and operation history.
Right before applying the recovery, it calculates the fingerprint again.
If the two don't match, the old recovery decision isn't applied. The data has to be compared again.
The local recovery itself is also atomic.
The restored data and pending upload operations are written together in a single IndexedDB transaction.
If something fails halfway through — for example, a required attachment is missing — the transaction is rolled back instead of leaving the app half-restored.
Basically:
all of it, or none of it.
This only protects the local recovery transaction. It doesn't lock another remote device from making changes while recovery is happening.
If another device changes the data afterward, the normal sync conflict rules take over again.
The hard part wasn't uploading data
When I started PlanMini, I thought avoiding a traditional backend would make the architecture much simpler.
That was partly true.
I don't have to run a database containing everyone's calendars, notes, and personal productivity data.
And the service can stay closer to the lightweight thing I originally wanted to build.
But the complexity didn't disappear.
It moved.
The difficult part of synchronization wasn't uploading or downloading data.
It was deciding what to do when both versions are valid.
Which changes are safe to merge?
When is something actually a conflict?
When should the software decide automatically?
And when should it stop and ask the user?
Working on PlanMini changed the way I think about sync quite a bit.
"Most recent" and "correct" aren't always the same thing.
When the app can't be confident about the answer, I prefer keeping both versions over silently throwing one away.
It's still a work in progress
PlanMini currently has a web version and a Windows desktop app.
It brings schedules, tasks, and notes together, lets notes be linked to calendar events, and uses Google Calendar and Google Drive for the underlying data.
The related sync and recovery logic currently has 42 passing tests, and the Electron TypeScript build also passes.
That doesn't mean I've reproduced every possible Google synchronization scenario across two real devices.
There are still cases I need to test and improve through actual use.
Funny enough, what started as:
"I just want a simple personal planner."
ended up taking me much deeper into synchronization than I expected.
PlanMini is currently free to use:
If you've built something around Google Calendar, Google Drive, or multi-device sync, I'd be interested to hear how you handled conflicts.



Top comments (0)