How Taskr is built
Taskr is a free todo app with List, Board and Calendar views. It has been running since December 2024, works offline, and never asks for an account before you add your first task. This page covers how it works underneath: where tasks are stored, how offline changes reach the server, and the bugs that taught the most.
Open Taskr Source on GitHubAt a glance
| App | React 18 and TypeScript, built with Create React App |
| On the device | IndexedDB for tasks and the offline queue, with a service worker so the app opens offline |
| Accounts | Supabase: Postgres for tasks, Supabase Auth for sign-in and password resets |
| Hosting | Vercel, with Vercel Web Analytics for anonymous page views |
| Drag and drop | dnd-kit for the board (mouse, touch and keyboard), FullCalendar for the calendar |
| Dates | chrono-node, loaded only when you start typing a task |
Local first, account optional
Every task is saved in your browser’s IndexedDB first. Without an account, that’s the only copy: nothing is sent anywhere, and the app works the same with or without a connection.
Creating an account adds Supabase on top. The device keeps its own copy of the account’s tasks, so a signed-in user still sees their list instantly, and offline.
- Guest tasks
- A copy of the account’s tasks
- An outbox of changes not yet sent
Signing in is optional. Guests only ever use the middle box.
Moving guest tasks into a new account
When a guest signs up, the tasks they already made come with them. Each task and subtask gets a fresh ID so it can’t clash with anything on the server. Two kinds are skipped: welcome tasks left exactly as they were (in current or older wording, so returning visitors are covered), and tasks the account already has.
Offline changes for signed-in users
A signed-in change that can’t reach Supabase goes into an outbox: a queue in IndexedDB, kept per account, that survives reloads. The device’s copy of the tasks is updated straight away, so nothing looks different to the user. The outbox is sent when the connection returns, when the app loads, on sign-in, and from the sync button.
- Oldest first, stopping at the first network failure, so an edit can never arrive before the task it edits.
- One send at a time. Two triggers firing together share the same run instead of sending twice.
- Retries are safe. If a change already reached the server before the connection dropped, the retry sees a duplicate key (Postgres
23505) or a missing row (PGRST116) and counts it as done. - Changes the server rejects are dropped, rather than blocking everything queued behind them.
Signing out sends what it can. If changes are still waiting, they and the account’s offline copy stay on the device until that person signs in again. Otherwise the copy is removed, so the next person on the device can’t see it.
When the database is asleep
Supabase’s free tier pauses a project after about a week without activity. Two things handle that. A GitHub Actions job makes one small read every three days to keep the project awake. And the app tells “the server is unreachable” apart from real errors (a failed fetch, a 5xx, or Supabase’s 540 “paused” response), so people see Cloud sync is asleep right now. Your tasks are still saved on this device. rather than a raw error.
Typing a task: natural language dates
Typing pay rent fri creates “pay rent” due on Friday, and dentist 3 Nov works the
same way. The parser, chrono-node, is a sizeable library, so it’s split into its own file and only downloaded
when you start typing. The first visit doesn’t pay for it.
Dates like 3/11 mean different things in different places. Taskr reads them month-first only
when the browser’s language is US English, and day-first everywhere else, which suits Taskr’s home in
Australia. A month or year on its own isn’t treated as a due date, so “call May about the lease” and
“watch 1917” stay as they are. A day, a weekday or a time has to be there too.
The Ctrl+K palette
Ctrl+K (⌘K on a Mac) opens a palette for switching views, importing, exporting
and jumping to any task. Matches that start with what you typed rank above matches in the middle of a word.
Unless the text is already a task, the last option creates it, with a live preview of any date it found.
Small details
Colours that travel round the colour wheel
When a task changes status, its colour strip doesn’t snap from yellow to blue. It sweeps round the colour
wheel, through the hues in between. The card usually moves to a different section, which creates a new element,
so an ordinary CSS transition has nothing to start from. Taskr remembers each task’s last status across
that move and plays a one-off animation. A registered custom property (@property) animates from
0 to 1, and color-mix(in hsl shorter hue, …) turns that number into the colour in between.
Dark mode without a white flash
A few lines of plain script in the page’s <head> apply the saved theme, or the device’s
light or dark setting, before React loads. Until someone picks a theme, Taskr follows the device, including
when it switches at sunset.
Settings that follow you
For signed-in users, the theme, the last view used and the calendar’s Month or Week choice are stored in the account’s Supabase user metadata, so no extra table is needed. A new device opens the way you left the last one. A change made offline is marked on the device and wins the next time it connects.
Deleting an account
A browser can’t delete a Supabase user directly. Taskr calls a small Postgres function instead. It runs with
elevated rights (SECURITY DEFINER), but can only ever delete the caller’s own tasks and account,
identified by auth.uid(). Afterwards the app clears that account’s copy and outbox from the device.
Usable without a mouse
Checkboxes and task rows are real buttons, so they’re reachable with Tab, and the account menu follows the WAI-ARIA menu button pattern, with arrow keys, Home, End and Escape. Cards can be dragged with a finger as well as a mouse.
There are also single-key shortcuts. Nothing in the app points to them, to keep the screen quiet; pressing
? anywhere in Taskr lists them (press Esc first if the cursor is in a text box).
j k | Next or previous task |
x | Complete or untick the task |
e | Edit it |
n | New task |
/ or Ctrl K | Search and commands |
1 2 3 | List, Board or Calendar |
Alt ↑ Alt ↓ | Move a task up or down the list |
← → ↑ ↓ | Move a card between and within columns on the board |
Ctrl-click, Shift-click | Select several tasks to move, complete or delete together |
Bugs worth telling
| What people saw | Why | The fix |
|---|---|---|
| New visitors got one welcome task instead of five | Each welcome task was saved on its own, and each save rewrote storage with just that task | Save the whole set in one write |
| Adding tasks in quick succession lost one | The save handler worked from a copy of the list captured before the previous add finished (a stale closure) | Read the latest list from a ref at the moment of saving |
| Signed-in edits made offline disappeared | The “back online” handler cleared the queue without sending it. The code even said “Here you would sync” | The outbox described above, tested against a fake Supabase server that can be switched off |
What’s next
- Automated tests. Import, export and date parsing were checked by hand with scripts. They should become a test suite.
- Moving off Create React App, which is no longer maintained, to a current build tool.
Everything that has shipped so far is in the changelog.