How to build a Todoist alternative
How to build a Todoist alternative is a Today list in your timezone, a quick-add line that previews the date, and a check-off that stays done after a refresh.
How to build a Todoist alternative is a capture line, a Today list, and a check-off that leaves the task in the database. Todoist is that product. The catalog line is “Capture the task, see today, check it off.”
The card lists $5–$8 a person a month. That is $60–$96 a year for every person. The verdict is “A weekend well spent”. The first estimate is 8 hours: quick add, a Today view, and check off. The project name on the card is Dolist.
The Todoist teardown is why a due day is a calendar date. This guide is the screens, the queries, and the prompts for that 8-hour row.
A team issue with an assignee is the Linear guide. Linear is $10–$16 a person a month, about 12 hours, and the Linear teardown is the other product. Hours you bill are the Toggl guide. Toggl Track is $9–$18 a person a month. An invoice you send for that work is the FreshBooks guide. Dolist is the open list on Friday morning.
How to build a Todoist alternative for one person
The Todoist page splits the work into three rows.
The first row is 8 hours. A text box that becomes a task. Inbox for anything with no day. Today for overdue and due today, in the owner’s timezone. A checkbox that writes a completed time and leaves the row in place.
The second row is 3 days, 24 hours. Recurring tasks, labels, saved filters, and one reminder email. The third row is a month and up, 160 hours. Native apps, offline sync, and calendar integration.
If you tell an agent to “clone Todoist”, it aims at the phone app. Paste the prompts below and keep Friday’s list on the first row.
What the 8-hour build does
You set the account timezone to Europe/Rome. You type Buy milk tomorrow p1 #errands on Friday 6 Nov 2026. The preview shows the title Buy milk, the date 2026-11-07, priority 1, and the project errands. Save writes that. Inbox does not show it. Today shows it on Saturday, and also shows anything overdue.
You type Renew flaviocopes.com with no day. It lands in Inbox. You check off Invoice Ada, due 2026-11-05. It leaves Today. A reload still has the row, with a completed time, off the open lists.
An unknown project name offers to create it. next thursdayish stays in the title and sets no date. A second check on the same task does not write a second completed time.
What it leaves out
No labels yet. @store stays in the title until the 24-hour row teaches the parser a new marker. No recurrence, no reminder email, no Upcoming, no saved filters, no subtasks, no search. No team workspace, no kanban, no habits, no karma, no location reminder, no phone app, and no calendar that writes events back.
The due value in this version is a calendar date. A time of day waits until recurrence needs a clock. Store the timezone on the account this weekend, because Today is already “what day is it for me.”
Where Today comes from
One browser, one logged-in owner. Node.js and Astro render the lists. Use TanStack Start if React is your weekday language. The stack post is which tab you copy. The card lists D1, so the projects and tasks live there. The same rules hold on a SQL database behind TanStack Start.
The quick-add preview is a request that does not write. The save request parses the raw line again and writes the task. One parser, two routes. If the chips and the saved row disagree, the save path is the one you trust, and the preview route is wrong. Do not let the browser be the only parser. A hand-edited post of “priority 1” with a raw line that never said p1 is refused.
Today is a query. Open tasks whose due day is before the owner’s local date are overdue. Open tasks whose due day equals that local date are today. Inbox is open tasks with a null due day. The browser does not send “this is overdue.” The server date in Europe/Rome decides.
Check-off is an update on that request. It sets completedAt only when the column is still null. A double click and a retry leave one timestamp. The task is not deleted.
Nothing runs in the background at 8 hours. The reminder email is a scheduled worker on the 24-hour row, and it is the first time Dolist talks to an email provider. If that worker dies, Today still loads. The list does not depend on mail.
Every query is scoped by owner. A guessed task id from another account returns the same empty result as a missing id. The timezone sits on the account. Tasks do not each carry a zone, because a date-only day has no zone. When you fly to New York and change the account to America/New_York, Friday stays Friday. The local “today” used for the query moves. The stored day does not.
The rows behind one task
The card lists Project, Task, Label, TaskLabel, Reminder, and Completion. This weekend you need the account timezone, Project, and Task. Completion becomes its own table when a repeating task has to keep history and also move forward. A boolean done is too thin for that later step, so v1 stores completedAt instead of a flag you would have to migrate.
A project is a named bucket. Inbox is a project the account starts with, and it is where a task goes when the line names no project. #errands finds or creates errands. Matching is case-insensitive. Creating a project from the line is one confirmation, then the task points at it.
A task has a title, a project, an optional priority from 1 to 4, an optional due day, and completedAt. The due day is YYYY-MM-DD or null. It is not a timestamp, and it is not midnight UTC. Priority empty sorts after p1, p2, p3, and p4.
Open means completedAt is null. Checking off fills that column with the server’s current instant. The due day stays. Leave un-complete off the 8-hour screens. The row is still there for you to read in a completed list once the 24-hour row adds history. Until then, a direct lookup by id shows the completed task, and Today hides it.
Labels are not a table yet. A reminder row is not a table yet. A recurrence rule is not a column yet. Leave room by keeping the due day a date. When the 24-hour row adds a rule, completing a weekly Monday task writes an immutable completion and sets the due day to the next Monday from the rule. A task completed on Wednesday still lands on Monday. That write is one transaction. v1 does not do it. v1’s completedAt is the habit the later table grows from: work you finished is a record, not a deleted row.
Sort on Today is overdue first, then due today. Inside a group, p1 comes before p2, p3, p4, then no priority. Inside a priority, older created time comes first. Inbox is newest first. Dragging to reorder can wait. A project page is the same rows filtered to that project, open ones only, same sort.
The screens you ship
Today, the quick-add line, Inbox, a project page, and a timezone setting. Upcoming can wait until Today has been boring for a week.
Today
Home. The quick-add line is in the header. Below it, overdue, then the owner’s local date as a heading. Each row is a checkbox, a title, the project name, and a priority mark if there is one. An empty day is the sentence “Nothing due”. No streak and no points.
On Friday 6 Nov 2026, with milk saved for Saturday, Today shows Invoice Ada under overdue if it is still open, and any task due 2026-11-06. Milk is absent until Saturday.
Quick add
One field. Under it, chips for title, due day, priority, and project. The chips update from the preview route. Save sends the raw line, not the chips. Cancel clears the field and writes nothing.
Buy milk tomorrow p1 #errands on that Friday becomes title Buy milk, due 2026-11-07, p1, project errands. #Errands matches the same project. A project named store that does not exist shows a create chip. Confirming creates it and then save assigns the task. Dismissing the create chip leaves #store in the title and sets no project, so the task falls through to Inbox.
\# keeps a hash character in the title. An empty title, after tokens are stripped, refuses save.
Inbox
Tasks with no due day. Renew flaviocopes.com and Call the accountant live here if you did not type a date. Each row can take a date, which moves it onto Today once that day is today or earlier, or can be checked off. A project name on the row is editable. Dating a row is a due day write, not a new task.
Project and timezone
The errands page lists open tasks in errands, dated and undated. It is a filter, not a second copy of the task. Settings is one control, the timezone, plus the examples the parser accepts: tomorrow, fri, next fri, jan 5. Show those examples on the setting so a wrong guess is something you can read before you type it.
From a typed line to a checked box
You set Europe/Rome. The local date for this walkthrough is Friday 6 Nov 2026. You type the milk line. The chips match. Save. Inbox does not list milk. Today does not list it either. Change the account’s “today” by waiting until Saturday, or by saving a task with an explicit day of 2026-11-06, and that one shows under Friday.
You check off Invoice Ada. The overdue group loses the row. Reload. The task still exists, completedAt filled, due day still 2026-11-05. Check it again. The timestamp does not move.
next thursdayish saves as a title with those words still in it, due day null, so it is in Inbox. p9 is not a priority. It stays in the title. #errands on a second milk task reuses the project. Two projects named errands means the match was case-sensitive or the create path did not look first.
If you post a save with chips that claim 2026-11-07 and a raw line of Buy milk with no date, the server stores no date. The preview was lying, or the client edited the chips. The raw line wins.
Date phrases you accept, evaluated on Friday 6 Nov 2026 in Europe/Rome:
tomorrow is 2026-11-07. today is 2026-11-06. fri is 2026-11-06, because today is Friday. next fri is 2026-11-13. jan 5 is 2027-01-05, because 5 Jan 2026 has passed. next thursdayish is not a date.
A weekday name means the next one, and if today is that weekday it means today. next plus a weekday means seven days after that coming weekday. If you cannot explain the phrase with one of those rules, leave it in the title.
Prompt 1. The account, Inbox, and Today
Copy the Astro prompt from Build your own Todoist. Paste this above it.
Ship the 8-hour row only. Project name: Dolist.
Node.js and Astro. Rows in D1.
TanStack Start is the same screens and the same rules,
with server functions and a SQL database.
One owner. Timezone on the account, default Europe/Rome.
Projects include a built-in Inbox.
Tasks have title, project, optional priority 1 to 4,
optional due day as YYYY-MM-DD, and completedAt.
Open means completedAt is null.
Today lists open tasks due before the owner's local date,
then open tasks due on that date.
Inbox lists open tasks with a null due day.
A project page filters the same rows.
Empty Today says Nothing due.
No labels, no recurrence, no email, no Upcoming,
no subtasks, no calendar, no phone app, no karma.
When the plan lists routes and tables, stop.
Do not store an overdue flag.
The due column is a date, not a timestamp.
What to check after the shell
Set the timezone to Europe/Rome and reload. It is still Europe/Rome. Inbox exists and has no tasks. Today says Nothing due. The tables are account, project, and task. There is no label table, no reminder table, and no overdue column. A logged-out window does not see the projects.
Prompt 2. The line and the preview
Add quick add to Dolist.
One raw line. A preview route parses it and writes nothing.
Save parses that same raw line again on the server.
Ignore posted chips if they disagree with the raw line.
Markers: #project, p1 through p4.
Dates, using the account timezone's local today:
tomorrow, today, a weekday name, next plus a weekday, jan 5.
Weekday means the coming one, or today if today matches.
next plus a weekday is seven days after that coming one.
jan 5 is later this year, or next year if that month-day has passed.
Anything else that looks like a date, including next thursdayish,
stays in the title and sets no due day.
p9 stays in the title.
A backslash before a hash keeps that hash in the title
and does not open a project.
An at-sign is not a marker in this version.
@store stays in the title.
Unknown project shows a create chip.
Confirm creates the project, then the task can use it.
Dismiss leaves the token in the title and uses Inbox.
An empty title after stripping tokens is refused.
Walkthrough clock for tests: local date Friday 2026-11-06.
Buy milk tomorrow p1 #errands
becomes title Buy milk, due 2026-11-07, p1, project errands.
Done when the preview chips match the row after save,
and next thursdayish has a null due day.
What to check after the parser
On the walkthrough date, the milk line previews 2026-11-07 and p1. Save it. The database due day is 2026-11-07, not a UTC midnight. next thursdayish is an Inbox task whose title still contains those words. p9 is in a title, and priority is null. #Errands does not create a second project. Buy milk \#review saves the title Buy milk #review and no project named review. A save whose raw line is Buy milk and whose chips claim a date stores a null due day. The preview route, called twice, inserts zero rows.
Prompt 3. Where the row shows up
Wire the lists to the saved tasks.
Milk due 2026-11-07 is absent from Today
when the local date is 2026-11-06,
and present on 2026-11-07.
A task due 2026-11-06 is under Friday, not under overdue.
A task due 2026-11-05 is under overdue.
A task due today is never overdue during that local date.
Inbox hides any task that has a due day.
Adding a due day on an Inbox row moves it.
Sort Today by overdue, then today,
then priority p1 to p4, then no priority,
then older created time first.
Inbox is newest first.
Changing the account timezone does not rewrite due days.
America/New_York still shows a 2026-11-06 task on 6 Nov
for the whole local date.
Done when milk, the Friday task, and the overdue task
land in those three places, and a timezone edit
leaves the stored dates unchanged.
What to check after the lists
Create Invoice Ada due 2026-11-05 in clients, Ship the guide due 2026-11-06 in writing, and the milk task. Today shows Ada, then Ship the guide. Milk is missing. Open Inbox and confirm milk is missing there too. Add a date of 2026-11-06 to Renew flaviocopes.com. It leaves Inbox and joins Friday. Switch the timezone to America/New_York. The three due days in the database are the same strings. Friday’s group still matches the local date you are testing, for the whole of that date.
Prompt 4. Check off without deleting
Add complete on Dolist.
The checkbox sets completedAt to the server time
only when it is null.
A second request leaves the timestamp as it was.
The due day stays.
The task row is not deleted.
Today and Inbox and project pages show open tasks only.
A direct open of the task still shows the completed row.
Do not advance a recurrence. There is no rule yet.
Do not send email.
Done when Invoice Ada leaves Today,
survives a reload, and a second check
keeps the original completedAt.
What to check after check-off
Check Ada. Reload Today. She is gone. Open the task by id. completedAt is set, due day is still 2026-11-05, project is still clients. Check again. The timestamp matches the first one. Count the task rows. It is the same count as before the check. A logged-out post to complete is refused. Another owner’s task id is refused.
Failures the agent ships anyway
Midnight UTC
The due column is a datetime. A task due Friday shows on Thursday evening in Rome, or on Saturday in New York. Read the stored value. If it has a T or a Z, it is an instant. Replace it with a date. Today compares that date to the owner’s local date and does nothing else with time zones on the task.
The row disappears
Check-off runs a delete. The count of tasks drops. Ada is gone from a lookup by id. Stop. Complete is an update. The 24-hour row cannot build history from a hole.
Two parsers
The preview shows 2026-11-07 and the saved row is null, or the other way around. The save route is parsing a different function, or trusting the chips. Call the same parser. Log a mismatch as a refused save if you want a signal, and still store only what the raw line produced. Do not log the title. Titles are personal. Log that a mismatch happened.
An overdue column
Today looks right, then you finish Ada and the column still says overdue, or you change the timezone and yesterday’s flag sticks. Overdue is the query. If a column flips when the page loads, delete the write.
Labels on day one
The schema grows a label join because the line had an @. The 8-hour row has no label screen. @store in the title is the behavior until the next row. A label table you did not prompt is how the weekend becomes the 24-hour row by accident.
Karma, a board, a calendar
A streak counter, five columns, or a Google connect button showed up beside Today. The non-goal names team workspaces, calendar sync, chat, kanban, habits, location reminders, and native apps. A board is the Trello guide. Trello is $10–$17.50 a person a month, about 3 hours, and it is a different list. Delete the extra screen and return to the checkbox.
The 24-hour row, after a real week
Use Today for a week as the list you actually open. Then add recurrence, labels, one saved filter, and the email.
Recurrence completes in one transaction. Write a completion row with the scheduled day and the completed time. Move the task’s due day from the rule, not from the day you tapped. A weekly Monday task finished on Wednesday is due next Monday. Monthly on the 31st clamps to the last day of a shorter month. A timed task, when you add times, keeps the local wall time across a daylight-saving change. Double-complete of that occurrence writes one history row and does not skip a period.
The email is one per owner per local date. Insert a unique key of owner and local date before you call the provider. A retry hits the key and stops. Build the body at send time. If nothing is due, send nothing, and still keep the key. Mail failure does not mark tasks done. Today worked before the worker existed, and it still works if the provider is down.
The prompt post is the extra line that says do not build the phone app. The scope post is the three-row habit.
Deploy, then use Friday
Dolist wants Astro and D1. Put it on a hostname you already own, today.flaviocopes.com. The hosting post is the adapter and the domain. Take a backup of D1 before you delete a paid account.
Log a save with the project id and whether a due day was set. Leave the title out. Log a complete with the task id and whether completedAt was already filled. When you add mail, log the local date and sent or skipped. Leave the task list out of the log line.
Then use it. Set Europe/Rome. Type five real tasks, one dated tomorrow and one with no date. Confirm Inbox and Today split them. Check one off. Refresh. It is gone from Today and still stored.
The same v1 also replaces TickTick, Things 3, and Any.do if what you wanted was a today list you type into. The card names them and does not give them prices.
What to skip on purpose
Leave native apps, offline sync, and a calendar that writes both ways. Leave team workspaces. ClickUp is $7–$12 a person a month if the rent is a shared board with assignees. That card is a 3-day project, and it is still not a phone app.
A planned day of calendar blocks is a different product from this capture box. Keep Dolist as the open tasks. Put the timer in Toggl. Put the invoice in Billfold.
Start now
Go to Build your own Todoist. Copy the Astro prompt, or TanStack Start if that is what you write.
Tell the agent the first estimate. Quick add, Today, check off. Eight hours. A due day is a calendar date. The parser on the server is the one that saves.
Type the milk line. See it land on the right day. Check one off, reload, and find the row still there.