All posts
Engineering6 min read

A due date with no time: how a day stays the same day in every time zone

A task due on the 5th should say the 5th in Delhi, in Dubai and in New York. It sounds like nothing. It was the bug behind a task that printed a day early for half the planet, and fixing it meant choosing what every date without a time means, and what the old ones meant.

Two encodings in one column

Due dates, deadlines and trip dates are often a day with no time, and the database stores them in a date-time column with no zone. The web app turned a picked day into local midnight in the browser. The Android app's calendar widget handed over its own timestamp, which was UTC midnight. Both landed in the same column looking like times, and a value that means midnight in London is the evening before in New York. The same row printed on different days depending on where it was read.

What a date-only value means now

Local midnight of the picked day, on both clients. A day is a day where the person who picked it was standing. The server never reinterprets it, and the row carries nothing about the zone it was written in, because nothing ever recorded one.

What the old rows mean

Existing rows could not be rewritten; nobody knows their author's zone. So the old encoding is recognised by its shape and its age: a value at exactly midnight UTC on a row created before 21 September 2026 is a date-only pick. After that cutoff, UTC midnight is a real time again, eight in the evening in New York and half past five in the morning in India, because no current client writes it to mean a day.

Printing a legacy row

A legacy row is printed as its UTC calendar day, relabelled as local, so it lands on its own day west of Greenwich as well as east. The same rule applies before an overdue check or a form pre-fill: the row is placed at local midnight of its UTC day first, then compared. Comparing the raw instant had marked legacy rows overdue a day early. Every caller that formats a date has to select the row's creation time alongside it; without it the row reads as legacy by default, which is the safer of the two mistakes.

What this costs

  • A phone on a build older than 2.1.3 that writes after the cutoff still stores UTC midnight, and on that phone those rows stay timed, as they always were.
  • A task genuinely due at midnight local, or at midnight UTC before the cutoff, reads as all-day. Accepted: it is a time nobody picks.
  • The server still cannot tell local midnight from a time without knowing the user's zone. That is why both clients report their zone to the account, and why the daily reminder counts your day in it.

Sources

The modules this touches

Try it on the free plan

No card. Both workspaces, unlimited tasks, the Planner, the Calendar and Focus.