Vibecode Todoist
track this build7 phases, 14 steps, beginner friendly0%A personal task app with projects, recurring due dates, filters, and reminders is a one-sitting/weekend build; collaboration and sync polish are the product moat.
You are building a lean indie version of Todoist.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Todoist · indie build
A personal task manager on localhost: quick-add that understands 'call Sam tomorrow 4pm #home p2', Today and Upcoming views, recurring tasks that behave on the 31st, full-text search, phone reminders through ntfy, and a CLI for capture from any terminal. One SQLite file you can back up.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | views, forms and a scheduler in one process |
| Parsing | chrono-node for dates, rrule for recurrence | calendar edge cases look simple until they are wrong on the last day of a month |
| Reminders | node-cron and ntfy | a phone notification with no app to build |
| Hosting | localhost, or your own network | your tasks, your machine |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **The ntfy app on your phone with a private topic** · free
- Why: Reminders arrive as push notifications through ntfy. No account needed; a long random topic name is the credential.
- Get it: Install ntfy from the App Store or Play Store, subscribe to a topic like tasks-<random 12 chars>, put the topic in .env. Test: curl -d 'hi' ntfy.sh/<topic>.
- Verify: The test message appears on the phone
- [ ] **Your IANA timezone** · free
- Why: Due dates are plain dates; reminders fire at local times.
- Get it: e.g. Europe/Berlin, into .env as TIMEZONE.
- [ ] **How 'every 3 days' should count: from the due date or from completion** · free
- Why: Both are defensible and users assume different ones. Decide the default now; Phase 4 offers the other per task.
- Get it: Default to due-date anchoring for scheduled chores; completion anchoring as a per-task flag for habits.
## Quick start
```sh
mkdir tasks && cd tasks && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Collaboration and shared projects.
- Native mobile apps and offline sync; phone access is ntfy plus the web UI on your network.
- Integrations and interaction polish.
- native apps
- offline sync
- collaboration
- reminders reliability
- integrations
- years of UX polish
If one of those is essential to you, that is the reason to keep paying for Todoist, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Todoist
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a personal task manager to replace Todoist. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- `chrono-node` for natural-language dates, `rrule` for recurrence. Do not
hand-roll either · both are full of calendar edge cases that look simple until
they are wrong on the last day of a month.
- Keyboard-first: `n` new, `x` complete, `/` search, `j`/`k` to move.
### Data model (create this before Phase 1)
- `tasks`: id, content, notes, project_id, priority (1-3), due_date
('YYYY-MM-DD', nullable), due_time (nullable), rrule (nullable), completed_at,
parent_id (nullable), position, created_at
- `projects`: id, name, color, archived
- `completions`: id, task_id, completed_at
Store `due_date` as a plain date string, not a timestamp. A task is due on a day,
and a timestamp will shift it across midnight for anyone who travels or observes
DST. Keep `completions` separate from `completed_at` so a recurring task has a
history rather than one ever-moving row.
### Phase 1 · Tasks and projects
Build: task CRUD, projects, priorities, notes, and completion. No parsing, no
dates yet · just the object model and a list that renders.
Done when: a task can be created, edited, completed and uncompleted; completing
writes a `completions` row; and archiving a project hides its tasks without
deleting them.
Do not build yet: parsing, recurrence, views, reminders.
### Phase 2 · Quick add with natural language
Build: the single-line parser. `call Sam tomorrow 4pm #home p2` sets content, due
date, due time, project and priority in one line. Use chrono-node for the date
portion only, after stripping `#project` and `pN` tokens · feeding those to the
parser produces confident nonsense. Show a live preview of what will be created
before the user commits, because a parser the user cannot see is a parser the
user stops trusting.
Done when: the example above parses correctly; `#home` does not end up in the
task title; "next friday" resolves to the right date near a month boundary; and
text with no date at all creates a task with no due date rather than today.
### Phase 3 · Views
Build: Today, Upcoming (next 7 days grouped by day) and per-project. Overdue
tasks pinned at the top of Today in red, and counted. Empty states that look
deliberate rather than broken.
Done when: a task due yesterday appears in Today as overdue, a task due in 3 days
appears under the right day heading in Upcoming, and Today renders correctly with
nothing due.
### Phase 4 · Recurrence
Build: recurrence with `rrule` ("every 1st", "every mon,thu"). Completing an
occurrence writes a `completions` row and schedules the next one. Decide and
document one rule: does "every 3 days" count from the due date or from the
completion date? Both are defensible and users assume different ones · pick
due-date-based for scheduled chores, offer completion-based as a per-task flag
for habits, and say which is which in the UI.
Done when: a monthly task due on the 31st behaves sanely in February, a weekly
task completed late schedules from the correct anchor per its flag, completing an
occurrence never destroys the series, and a recurring task's history is visible.
### Phase 5 · Search and subtasks
Build: full-text search across content and notes with SQLite FTS5, and one level
of subtasks (`parent_id`), where completing a parent does not silently complete
children.
Done when: search finds a phrase inside a note, results are ranked usefully, and
subtask completion state is independent and visible on the parent.
### Phase 6 · Reminders
Build: a `node-cron` loop checking each minute, pushing due tasks to a phone via
ntfy (topic in `.env`), plus a 7am digest of the day's list. Deduplicate · a task
must notify once, not every minute until completed. Survive a restart without
re-firing everything already sent.
Done when: a task due at 14:00 notifies once at 14:00, restarting the process at
14:30 does not re-notify, and the digest arrives once with the correct list.
### Phase 7 · CLI and deploy
Build: a `t add "..."` CLI hitting the same parser so capture works from any
terminal, a nightly database backup keeping 30, a systemd unit, and the README.
Done when: the CLI creates a task identical to one created in the UI, and a
restart loses nothing.
### Out of scope (and why)
- Collaboration and shared projects.
- Native mobile apps and offline sync. Phone access is ntfy notifications and the
web UI over your network · that is the trade, and it is a real downgrade from
a native app that works on a plane. Say so.
- Integrations and years of interaction polish.
### README must contain
- The quick-add syntax, with examples including one that is deliberately ambiguous.
- The recurrence anchor rule, both modes, and how to set the flag.
- ntfy setup on the phone, and where the database lives.
===== AGENTS.md =====
# Agent instructions · Todoist indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, chrono-node for dates, rrule for recurrence, node-cron and ntfy, localhost, or your own network. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · Todoist
A personal task manager on localhost: quick-add that understands 'call Sam tomorrow 4pm #home p2', Today and Upcoming views, recurring tasks that behave on the 31st, full-text search, phone reminders through ntfy, and a CLI for capture from any terminal. One SQLite file you can back up.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Tasks and projects
The object model and a list that renders; no parsing yet.
### Steps
1. Create the project and the tables
tasks (id, content, notes, project_id, priority 1-3, due_date YYYY-MM-DD, due_time, rrule, anchor, completed_at, parent_id, position, created_at), projects (id, name, color, archived), completions (id, task_id, completed_at).
```sh
mkdir tasks && cd tasks && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
2. Build task CRUD, projects, priorities, notes and completion
Completing writes a completions row. Archiving a project hides its tasks without deleting.
### Done when
- [ ] A task can be created, edited, completed and uncompleted
- [ ] Completing writes a completions row
- [ ] Archiving a project hides its tasks without deleting them
## Phase 2 · Quick add with natural language
One line becomes a task with a date, project and priority, previewed before saving.
### Steps
1. Install chrono-node and write the parser
Strip #project and pN tokens first, then hand the rest to chrono. Feeding those tokens to the parser produces confident nonsense.
```sh
npm install chrono-node@2
```
2. Show a live preview of what will be created before commit
### Done when
- [ ] 'call Sam tomorrow 4pm #home p2' parses correctly
- [ ] #home does not end up in the title
- [ ] 'next friday' resolves correctly near a month boundary
- [ ] Text with no date creates a task with no due date, not today
## Phase 3 · Views
Today, Upcoming and per-project, with overdue pinned.
### Steps
1. Build Today with overdue tasks pinned red at the top and counted
2. Build Upcoming grouped by day for seven days, and per-project views, each with a deliberate empty state
### Done when
- [ ] A task due yesterday appears in Today as overdue
- [ ] A task due in 3 days appears under the right day in Upcoming
- [ ] Today renders correctly with nothing due
## Phase 4 · Recurrence
rrule strings, the anchor rule you decided, and sane behaviour in February.
### Steps
1. Install rrule and store recurrence as RRULE strings
```sh
npm install rrule@2
```
2. Implement the anchor flag and scheduling of the next occurrence on completion
Completing writes a completions row and moves due_date to the next occurrence per the anchor.
### Done when
- [ ] A monthly task due on the 31st behaves sanely in February
- [ ] A weekly task completed late schedules from the correct anchor per its flag
- [ ] Completing never destroys the series
- [ ] History is visible
## Phase 5 · Search and subtasks
FTS5 search and one level of subtasks.
### Steps
1. Add an FTS5 table over content and notes, kept in sync by triggers
2. Add one level of subtasks via parent_id with independent completion
### Done when
- [ ] Search finds a phrase inside a note
- [ ] Completing a parent does not complete children
- [ ] Subtask state is visible on the parent
## Phase 6 · Reminders
Once per task, surviving restarts, plus a morning digest.
### Steps
1. node-cron every minute: push due tasks to ntfy once
Record notified_at on the task so a restart never re-fires.
```sh
npm install node-cron@3
curl -d 'test' ntfy.sh/$NTFY_TOPIC
```
2. Send the DIGEST_HOUR digest with the day's list
### Done when
- [ ] A task due at 14:00 notifies once at 14:00
- [ ] Restarting at 14:30 does not re-notify
- [ ] The digest arrives once with the correct list
## Phase 7 · CLI and deploy
Capture from any terminal, backups, a service.
### Steps
1. Write t add "..." using the same parser
```sh
npm link
t add "buy milk tomorrow #home"
```
2. Nightly backup keeping thirty, a systemd unit, the README
README: quick-add syntax with an ambiguous example, the anchor rule and flag, ntfy setup, where the database lives.
Files: `README.md`
### Done when
- [ ] The CLI creates a task identical to the UI
- [ ] A restart loses nothing
## Not in this build
- Collaboration and shared projects.
- Native mobile apps and offline sync; phone access is ntfy plus the web UI on your network.
- Integrations and interaction polish.
## After v1, if you want it
- A PWA shell for the phone
- Natural-language recurrence ('every other tuesday') mapped to rrule
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1 or your LAN address.
PORT=4810
# Required. SQLite file.
DATABASE_PATH=./data/tasks.db
# Required. IANA zone for reminders and the digest.
TIMEZONE=Europe/Berlin
# Required · secret. Your private topic from the ntfy app.
NTFY_TOPIC=tasks-a8f3k2m9x1q7
# Optional. Hour of the morning digest in TIMEZONE.
DIGEST_HOUR=7
You are building a lean indie version of Todoist.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== README.md =====
# Todoist · indie build
A personal task manager on localhost: quick-add that understands 'call Sam tomorrow 4pm #home p2', Today and Upcoming views, recurring tasks that behave on the 31st, full-text search, phone reminders through ntfy, and a CLI for capture from any terminal. One SQLite file you can back up.
Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts.
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | views, forms and a scheduler in one process |
| Parsing | chrono-node for dates, rrule for recurrence | calendar edge cases look simple until they are wrong on the last day of a month |
| Reminders | node-cron and ntfy | a phone notification with no app to build |
| Hosting | localhost, or your own network | your tasks, your machine |
## Before you start
Have every one of these ready. The plan assumes them from step one.
- [ ] **Node.js 22 or newer** · free
- Why: Everything in this build runs on it: the server, the scripts, the tests.
- Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards.
- Verify: node --version prints v22 or higher
- [ ] **A terminal and a code editor** · free
- Why: Every step below is a command you type or a file you edit.
- Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal.
- Verify: You can open a folder and run a command in its terminal
- [ ] **Git** · free
- Why: History for your code, and the way most hosts deploy.
- Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists.
- Verify: git --version prints a version
- [ ] **The ntfy app on your phone with a private topic** · free
- Why: Reminders arrive as push notifications through ntfy. No account needed; a long random topic name is the credential.
- Get it: Install ntfy from the App Store or Play Store, subscribe to a topic like tasks-<random 12 chars>, put the topic in .env. Test: curl -d 'hi' ntfy.sh/<topic>.
- Verify: The test message appears on the phone
- [ ] **Your IANA timezone** · free
- Why: Due dates are plain dates; reminders fire at local times.
- Get it: e.g. Europe/Berlin, into .env as TIMEZONE.
- [ ] **How 'every 3 days' should count: from the due date or from completion** · free
- Why: Both are defensible and users assume different ones. Decide the default now; Phase 4 offers the other per task.
- Get it: Default to due-date anchoring for scheduled chores; completion anchoring as a per-task flag for habits.
## Quick start
```sh
mkdir tasks && cd tasks && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
Then copy `.env.example` to `.env` and fill in the values it documents.
## Honest limits
This build deliberately does not replace:
- Collaboration and shared projects.
- Native mobile apps and offline sync; phone access is ntfy plus the web UI on your network.
- Integrations and interaction polish.
- native apps
- offline sync
- collaboration
- reminders reliability
- integrations
- years of UX polish
If one of those is essential to you, that is the reason to keep paying for Todoist, and the README should say so rather than pretend.
===== BRIEF.md =====
# Build brief · Todoist
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a personal task manager to replace Todoist. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- `chrono-node` for natural-language dates, `rrule` for recurrence. Do not
hand-roll either · both are full of calendar edge cases that look simple until
they are wrong on the last day of a month.
- Keyboard-first: `n` new, `x` complete, `/` search, `j`/`k` to move.
### Data model (create this before Phase 1)
- `tasks`: id, content, notes, project_id, priority (1-3), due_date
('YYYY-MM-DD', nullable), due_time (nullable), rrule (nullable), completed_at,
parent_id (nullable), position, created_at
- `projects`: id, name, color, archived
- `completions`: id, task_id, completed_at
Store `due_date` as a plain date string, not a timestamp. A task is due on a day,
and a timestamp will shift it across midnight for anyone who travels or observes
DST. Keep `completions` separate from `completed_at` so a recurring task has a
history rather than one ever-moving row.
### Phase 1 · Tasks and projects
Build: task CRUD, projects, priorities, notes, and completion. No parsing, no
dates yet · just the object model and a list that renders.
Done when: a task can be created, edited, completed and uncompleted; completing
writes a `completions` row; and archiving a project hides its tasks without
deleting them.
Do not build yet: parsing, recurrence, views, reminders.
### Phase 2 · Quick add with natural language
Build: the single-line parser. `call Sam tomorrow 4pm #home p2` sets content, due
date, due time, project and priority in one line. Use chrono-node for the date
portion only, after stripping `#project` and `pN` tokens · feeding those to the
parser produces confident nonsense. Show a live preview of what will be created
before the user commits, because a parser the user cannot see is a parser the
user stops trusting.
Done when: the example above parses correctly; `#home` does not end up in the
task title; "next friday" resolves to the right date near a month boundary; and
text with no date at all creates a task with no due date rather than today.
### Phase 3 · Views
Build: Today, Upcoming (next 7 days grouped by day) and per-project. Overdue
tasks pinned at the top of Today in red, and counted. Empty states that look
deliberate rather than broken.
Done when: a task due yesterday appears in Today as overdue, a task due in 3 days
appears under the right day heading in Upcoming, and Today renders correctly with
nothing due.
### Phase 4 · Recurrence
Build: recurrence with `rrule` ("every 1st", "every mon,thu"). Completing an
occurrence writes a `completions` row and schedules the next one. Decide and
document one rule: does "every 3 days" count from the due date or from the
completion date? Both are defensible and users assume different ones · pick
due-date-based for scheduled chores, offer completion-based as a per-task flag
for habits, and say which is which in the UI.
Done when: a monthly task due on the 31st behaves sanely in February, a weekly
task completed late schedules from the correct anchor per its flag, completing an
occurrence never destroys the series, and a recurring task's history is visible.
### Phase 5 · Search and subtasks
Build: full-text search across content and notes with SQLite FTS5, and one level
of subtasks (`parent_id`), where completing a parent does not silently complete
children.
Done when: search finds a phrase inside a note, results are ranked usefully, and
subtask completion state is independent and visible on the parent.
### Phase 6 · Reminders
Build: a `node-cron` loop checking each minute, pushing due tasks to a phone via
ntfy (topic in `.env`), plus a 7am digest of the day's list. Deduplicate · a task
must notify once, not every minute until completed. Survive a restart without
re-firing everything already sent.
Done when: a task due at 14:00 notifies once at 14:00, restarting the process at
14:30 does not re-notify, and the digest arrives once with the correct list.
### Phase 7 · CLI and deploy
Build: a `t add "..."` CLI hitting the same parser so capture works from any
terminal, a nightly database backup keeping 30, a systemd unit, and the README.
Done when: the CLI creates a task identical to one created in the UI, and a
restart loses nothing.
### Out of scope (and why)
- Collaboration and shared projects.
- Native mobile apps and offline sync. Phone access is ntfy notifications and the
web UI over your network · that is the trade, and it is a real downgrade from
a native app that works on a plane. Say so.
- Integrations and years of interaction polish.
### README must contain
- The quick-add syntax, with examples including one that is deliberately ambiguous.
- The recurrence anchor rule, both modes, and how to set the flag.
- ntfy setup on the phone, and where the database lives.
===== AGENTS.md =====
# Agent instructions · Todoist indie build
- Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, chrono-node for dates, rrule for recurrence, node-cron and ntfy, localhost, or your own network. Do not substitute.
- Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes.
- Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name.
- Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced.
- Do not invent cryptography, security guarantees, APIs or compliance claims.
- Add a focused test for every destructive, security-sensitive or data-loss path the plan names.
- Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
===== BUILD_PLAN.md =====
# Build plan · Todoist
A personal task manager on localhost: quick-add that understands 'call Sam tomorrow 4pm #home p2', Today and Upcoming views, recurring tasks that behave on the 31st, full-text search, phone reminders through ntfy, and a CLI for capture from any terminal. One SQLite file you can back up.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Tasks and projects
The object model and a list that renders; no parsing yet.
### Steps
1. Create the project and the tables
tasks (id, content, notes, project_id, priority 1-3, due_date YYYY-MM-DD, due_time, rrule, anchor, completed_at, parent_id, position, created_at), projects (id, name, color, archived), completions (id, task_id, completed_at).
```sh
mkdir tasks && cd tasks && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
2. Build task CRUD, projects, priorities, notes and completion
Completing writes a completions row. Archiving a project hides its tasks without deleting.
### Done when
- [ ] A task can be created, edited, completed and uncompleted
- [ ] Completing writes a completions row
- [ ] Archiving a project hides its tasks without deleting them
## Phase 2 · Quick add with natural language
One line becomes a task with a date, project and priority, previewed before saving.
### Steps
1. Install chrono-node and write the parser
Strip #project and pN tokens first, then hand the rest to chrono. Feeding those tokens to the parser produces confident nonsense.
```sh
npm install chrono-node@2
```
2. Show a live preview of what will be created before commit
### Done when
- [ ] 'call Sam tomorrow 4pm #home p2' parses correctly
- [ ] #home does not end up in the title
- [ ] 'next friday' resolves correctly near a month boundary
- [ ] Text with no date creates a task with no due date, not today
## Phase 3 · Views
Today, Upcoming and per-project, with overdue pinned.
### Steps
1. Build Today with overdue tasks pinned red at the top and counted
2. Build Upcoming grouped by day for seven days, and per-project views, each with a deliberate empty state
### Done when
- [ ] A task due yesterday appears in Today as overdue
- [ ] A task due in 3 days appears under the right day in Upcoming
- [ ] Today renders correctly with nothing due
## Phase 4 · Recurrence
rrule strings, the anchor rule you decided, and sane behaviour in February.
### Steps
1. Install rrule and store recurrence as RRULE strings
```sh
npm install rrule@2
```
2. Implement the anchor flag and scheduling of the next occurrence on completion
Completing writes a completions row and moves due_date to the next occurrence per the anchor.
### Done when
- [ ] A monthly task due on the 31st behaves sanely in February
- [ ] A weekly task completed late schedules from the correct anchor per its flag
- [ ] Completing never destroys the series
- [ ] History is visible
## Phase 5 · Search and subtasks
FTS5 search and one level of subtasks.
### Steps
1. Add an FTS5 table over content and notes, kept in sync by triggers
2. Add one level of subtasks via parent_id with independent completion
### Done when
- [ ] Search finds a phrase inside a note
- [ ] Completing a parent does not complete children
- [ ] Subtask state is visible on the parent
## Phase 6 · Reminders
Once per task, surviving restarts, plus a morning digest.
### Steps
1. node-cron every minute: push due tasks to ntfy once
Record notified_at on the task so a restart never re-fires.
```sh
npm install node-cron@3
curl -d 'test' ntfy.sh/$NTFY_TOPIC
```
2. Send the DIGEST_HOUR digest with the day's list
### Done when
- [ ] A task due at 14:00 notifies once at 14:00
- [ ] Restarting at 14:30 does not re-notify
- [ ] The digest arrives once with the correct list
## Phase 7 · CLI and deploy
Capture from any terminal, backups, a service.
### Steps
1. Write t add "..." using the same parser
```sh
npm link
t add "buy milk tomorrow #home"
```
2. Nightly backup keeping thirty, a systemd unit, the README
README: quick-add syntax with an ambiguous example, the anchor rule and flag, ntfy setup, where the database lives.
Files: `README.md`
### Done when
- [ ] The CLI creates a task identical to the UI
- [ ] A restart loses nothing
## Not in this build
- Collaboration and shared projects.
- Native mobile apps and offline sync; phone access is ntfy plus the web UI on your network.
- Integrations and interaction polish.
## After v1, if you want it
- A PWA shell for the phone
- Natural-language recurrence ('every other tuesday') mapped to rrule
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1 or your LAN address.
PORT=4810
# Required. SQLite file.
DATABASE_PATH=./data/tasks.db
# Required. IANA zone for reminders and the digest.
TIMEZONE=Europe/Berlin
# Required · secret. Your private topic from the ntfy app.
NTFY_TOPIC=tasks-a8f3k2m9x1q7
# Optional. Hour of the morning digest in TIMEZONE.
DIGEST_HOUR=7
You are building a production product version of Todoist.
Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt.
===== PRODUCT.md =====
# Todoist · product brief
## Problem
A personal task app with projects, recurring due dates, filters, and reminders is a one-sitting/weekend build; collaboration and sync polish are the product moat.
## Product outcome
A task manager you could run for your household on your network: reliable reminders, recurrence that is explainable, capture from anywhere.
## Target user
A builder who needs a maintainable product foundation, not a one-off demo.
## Required capabilities
- local or hosted database
- date parser
- notification channel
- optional mobile/desktop wrappers
## Explicit non-goals for v1
- Collaboration and shared projects.
- Native mobile apps and offline sync; phone access is ntfy plus the web UI on your network.
- Integrations and interaction polish.
- native apps
- offline sync
- collaboration
- reminders reliability
- integrations
- years of UX polish
## Success criteria
- Recurrence verified on the 31st, across DST, and late completion
- Reminders verified once-only across a restart
- One restore drill performed
===== BRIEF.md =====
# Build brief · Todoist
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a personal task manager to replace Todoist. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- `chrono-node` for natural-language dates, `rrule` for recurrence. Do not
hand-roll either · both are full of calendar edge cases that look simple until
they are wrong on the last day of a month.
- Keyboard-first: `n` new, `x` complete, `/` search, `j`/`k` to move.
### Data model (create this before Phase 1)
- `tasks`: id, content, notes, project_id, priority (1-3), due_date
('YYYY-MM-DD', nullable), due_time (nullable), rrule (nullable), completed_at,
parent_id (nullable), position, created_at
- `projects`: id, name, color, archived
- `completions`: id, task_id, completed_at
Store `due_date` as a plain date string, not a timestamp. A task is due on a day,
and a timestamp will shift it across midnight for anyone who travels or observes
DST. Keep `completions` separate from `completed_at` so a recurring task has a
history rather than one ever-moving row.
### Phase 1 · Tasks and projects
Build: task CRUD, projects, priorities, notes, and completion. No parsing, no
dates yet · just the object model and a list that renders.
Done when: a task can be created, edited, completed and uncompleted; completing
writes a `completions` row; and archiving a project hides its tasks without
deleting them.
Do not build yet: parsing, recurrence, views, reminders.
### Phase 2 · Quick add with natural language
Build: the single-line parser. `call Sam tomorrow 4pm #home p2` sets content, due
date, due time, project and priority in one line. Use chrono-node for the date
portion only, after stripping `#project` and `pN` tokens · feeding those to the
parser produces confident nonsense. Show a live preview of what will be created
before the user commits, because a parser the user cannot see is a parser the
user stops trusting.
Done when: the example above parses correctly; `#home` does not end up in the
task title; "next friday" resolves to the right date near a month boundary; and
text with no date at all creates a task with no due date rather than today.
### Phase 3 · Views
Build: Today, Upcoming (next 7 days grouped by day) and per-project. Overdue
tasks pinned at the top of Today in red, and counted. Empty states that look
deliberate rather than broken.
Done when: a task due yesterday appears in Today as overdue, a task due in 3 days
appears under the right day heading in Upcoming, and Today renders correctly with
nothing due.
### Phase 4 · Recurrence
Build: recurrence with `rrule` ("every 1st", "every mon,thu"). Completing an
occurrence writes a `completions` row and schedules the next one. Decide and
document one rule: does "every 3 days" count from the due date or from the
completion date? Both are defensible and users assume different ones · pick
due-date-based for scheduled chores, offer completion-based as a per-task flag
for habits, and say which is which in the UI.
Done when: a monthly task due on the 31st behaves sanely in February, a weekly
task completed late schedules from the correct anchor per its flag, completing an
occurrence never destroys the series, and a recurring task's history is visible.
### Phase 5 · Search and subtasks
Build: full-text search across content and notes with SQLite FTS5, and one level
of subtasks (`parent_id`), where completing a parent does not silently complete
children.
Done when: search finds a phrase inside a note, results are ranked usefully, and
subtask completion state is independent and visible on the parent.
### Phase 6 · Reminders
Build: a `node-cron` loop checking each minute, pushing due tasks to a phone via
ntfy (topic in `.env`), plus a 7am digest of the day's list. Deduplicate · a task
must notify once, not every minute until completed. Survive a restart without
re-firing everything already sent.
Done when: a task due at 14:00 notifies once at 14:00, restarting the process at
14:30 does not re-notify, and the digest arrives once with the correct list.
### Phase 7 · CLI and deploy
Build: a `t add "..."` CLI hitting the same parser so capture works from any
terminal, a nightly database backup keeping 30, a systemd unit, and the README.
Done when: the CLI creates a task identical to one created in the UI, and a
restart loses nothing.
### Out of scope (and why)
- Collaboration and shared projects.
- Native mobile apps and offline sync. Phone access is ntfy notifications and the
web UI over your network · that is the trade, and it is a real downgrade from
a native app that works on a plane. Say so.
- Integrations and years of interaction polish.
### README must contain
- The quick-add syntax, with examples including one that is deliberately ambiguous.
- The recurrence anchor rule, both modes, and how to set the flag.
- ntfy setup on the phone, and where the database lives.
===== ARCHITECTURE.md =====
# Architecture · Todoist
## Stack
| Part | Choice | Why |
| --- | --- | --- |
| Runtime | Node 22 with Express and better-sqlite3 | views, forms and a scheduler in one process |
| Parsing | chrono-node for dates, rrule for recurrence | calendar edge cases look simple until they are wrong on the last day of a month |
| Reminders | node-cron and ntfy | a phone notification with no app to build |
| Hosting | localhost, or your own network | your tasks, your machine |
## Modules
Each module has one owner concern and a documented way to replace it.
| Module | Owns | How to replace it |
| --- | --- | --- |
| Store | tasks, projects, completions, FTS | The core |
| Parser | quick-add tokens and chrono | Any parser producing the same fields |
| Recurrence | rrule and anchors | Rules only |
| Notify | node-cron and ntfy | Any push channel behind notify() |
## Configuration
Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists.
- `PORT` · required · Bound to 127.0.0.1 or your LAN address.
- `DATABASE_PATH` · required · SQLite file.
- `TIMEZONE` · required · IANA zone for reminders and the digest.
- `NTFY_TOPIC` · required, secret · Your private topic from the ntfy app.
- `DIGEST_HOUR` · optional · Hour of the morning digest in TIMEZONE.
## Production baseline
- Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives.
- Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised.
- Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts.
- Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists.
- Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path.
## Decision records
For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
===== AGENTS.md =====
# Agent instructions · Todoist product build
- Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with Express and better-sqlite3, chrono-node for dates, rrule for recurrence, node-cron and ntfy, localhost, or your own network.
- Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit.
- Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present.
- Never invent cryptography or silently weaken a requirement to make a check pass.
- Put every external service behind an interface with a deterministic fake for tests.
- Add migrations and rollback or recovery notes for every persistent data change.
- Log useful operational context without credentials, tokens, passwords or personal data.
- Update documentation and run every check before completing a milestone.
===== MILESTONES.md =====
# Delivery milestones · Todoist
Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer.
## M1 · Tasks and projects
The object model and a list that renders; no parsing yet.
### Steps
1. Create the project and the tables
tasks (id, content, notes, project_id, priority 1-3, due_date YYYY-MM-DD, due_time, rrule, anchor, completed_at, parent_id, position, created_at), projects (id, name, color, archived), completions (id, task_id, completed_at).
```sh
mkdir tasks && cd tasks && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
2. Build task CRUD, projects, priorities, notes and completion
Completing writes a completions row. Archiving a project hides its tasks without deleting.
### Done when
- [ ] A task can be created, edited, completed and uncompleted
- [ ] Completing writes a completions row
- [ ] Archiving a project hides its tasks without deleting them
## M2 · Quick add with natural language
One line becomes a task with a date, project and priority, previewed before saving.
### Steps
1. Install chrono-node and write the parser
Strip #project and pN tokens first, then hand the rest to chrono. Feeding those tokens to the parser produces confident nonsense.
```sh
npm install chrono-node@2
```
2. Show a live preview of what will be created before commit
### Done when
- [ ] 'call Sam tomorrow 4pm #home p2' parses correctly
- [ ] #home does not end up in the title
- [ ] 'next friday' resolves correctly near a month boundary
- [ ] Text with no date creates a task with no due date, not today
## M3 · Views
Today, Upcoming and per-project, with overdue pinned.
### Steps
1. Build Today with overdue tasks pinned red at the top and counted
2. Build Upcoming grouped by day for seven days, and per-project views, each with a deliberate empty state
### Done when
- [ ] A task due yesterday appears in Today as overdue
- [ ] A task due in 3 days appears under the right day in Upcoming
- [ ] Today renders correctly with nothing due
## M4 · Recurrence
rrule strings, the anchor rule you decided, and sane behaviour in February.
### Steps
1. Install rrule and store recurrence as RRULE strings
```sh
npm install rrule@2
```
2. Implement the anchor flag and scheduling of the next occurrence on completion
Completing writes a completions row and moves due_date to the next occurrence per the anchor.
### Done when
- [ ] A monthly task due on the 31st behaves sanely in February
- [ ] A weekly task completed late schedules from the correct anchor per its flag
- [ ] Completing never destroys the series
- [ ] History is visible
## M5 · Search and subtasks
FTS5 search and one level of subtasks.
### Steps
1. Add an FTS5 table over content and notes, kept in sync by triggers
2. Add one level of subtasks via parent_id with independent completion
### Done when
- [ ] Search finds a phrase inside a note
- [ ] Completing a parent does not complete children
- [ ] Subtask state is visible on the parent
## M6 · Reminders
Once per task, surviving restarts, plus a morning digest.
### Steps
1. node-cron every minute: push due tasks to ntfy once
Record notified_at on the task so a restart never re-fires.
```sh
npm install node-cron@3
curl -d 'test' ntfy.sh/$NTFY_TOPIC
```
2. Send the DIGEST_HOUR digest with the day's list
### Done when
- [ ] A task due at 14:00 notifies once at 14:00
- [ ] Restarting at 14:30 does not re-notify
- [ ] The digest arrives once with the correct list
## M7 · CLI and deploy
Capture from any terminal, backups, a service.
### Steps
1. Write t add "..." using the same parser
```sh
npm link
t add "buy milk tomorrow #home"
```
2. Nightly backup keeping thirty, a systemd unit, the README
README: quick-add syntax with an ambiguous example, the anchor rule and flag, ntfy setup, where the database lives.
Files: `README.md`
### Done when
- [ ] The CLI creates a task identical to the UI
- [ ] A restart loses nothing
## M8 · Operate it like a product (production only)
Only for the product-builder path: know when the task server is down, never lose the database, and keep the server patched.
### Steps
1. Add a /healthz endpoint and an external uptime check against it
Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices.
2. Write structured request logs and rotate them
One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight.
3. Back the SQLite file up off the machine nightly and test a restore
SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it.
```sh
sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'"
rclone copy /tmp/app-$(date +%F).db remote:backups/
```
4. Lock the box down
Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure.
### Done when
- [ ] Stopping the service triggers an uptime alert within a few minutes
- [ ] A restore from last night's backup contains yesterday's data
- [ ] A port scan from another machine shows only 22, 80 and 443
===== OPERATIONS.md =====
# Operations · Todoist
## Backup
SQLite .backup nightly.
## Restore
Copy back; check Today.
Do a restore drill before the first real user, and write the date here when it passes.
## Monitoring
Uptime on /healthz; a missed digest is the alarm.
## Incident checklist
If the ntfy topic leaks, pick a new one; nothing else is exposed.
1. Contain the issue without destroying evidence or user data.
2. Record the timeline and affected scope.
3. Rotate exposed secrets and revoke compromised sessions or credentials.
4. Restore from a verified backup when needed.
5. Document the root cause, the remediation and the regression test.
## Release gate
- [ ] Recurrence verified on the 31st, across DST, and late completion
- [ ] Reminders verified once-only across a restart
- [ ] One restore drill performed
## Launch constraint
Do not market omitted Todoist capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
===== .env.example =====
# Copy to .env and fill in. Never commit .env; this file documents it.
# Required. Bound to 127.0.0.1 or your LAN address.
PORT=4810
# Required. SQLite file.
DATABASE_PATH=./data/tasks.db
# Required. IANA zone for reminders and the digest.
TIMEZONE=Europe/Berlin
# Required · secret. Your private topic from the ntfy app.
NTFY_TOPIC=tasks-a8f3k2m9x1q7
# Optional. Hour of the morning digest in TIMEZONE.
DIGEST_HOUR=7
# Todoist · indie build A personal task manager on localhost: quick-add that understands 'call Sam tomorrow 4pm #home p2', Today and Upcoming views, recurring tasks that behave on the 31st, full-text search, phone reminders through ntfy, and a CLI for capture from any terminal. One SQLite file you can back up. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with Express and better-sqlite3 | views, forms and a scheduler in one process | | Parsing | chrono-node for dates, rrule for recurrence | calendar edge cases look simple until they are wrong on the last day of a month | | Reminders | node-cron and ntfy | a phone notification with no app to build | | Hosting | localhost, or your own network | your tasks, your machine | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **The ntfy app on your phone with a private topic** · free - Why: Reminders arrive as push notifications through ntfy. No account needed; a long random topic name is the credential. - Get it: Install ntfy from the App Store or Play Store, subscribe to a topic like tasks-<random 12 chars>, put the topic in .env. Test: curl -d 'hi' ntfy.sh/<topic>. - Verify: The test message appears on the phone - [ ] **Your IANA timezone** · free - Why: Due dates are plain dates; reminders fire at local times. - Get it: e.g. Europe/Berlin, into .env as TIMEZONE. - [ ] **How 'every 3 days' should count: from the due date or from completion** · free - Why: Both are defensible and users assume different ones. Decide the default now; Phase 4 offers the other per task. - Get it: Default to due-date anchoring for scheduled chores; completion anchoring as a per-task flag for habits. ## Quick start ```sh mkdir tasks && cd tasks && git init && npm init -y && npm pkg set type=module npm install express@4 better-sqlite3@13 mkdir data && cp .env.example .env ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Collaboration and shared projects. - Native mobile apps and offline sync; phone access is ntfy plus the web UI on your network. - Integrations and interaction polish. - native apps - offline sync - collaboration - reminders reliability - integrations - years of UX polish If one of those is essential to you, that is the reason to keep paying for Todoist, and the README should say so rather than pretend.
# Build brief · Todoist
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a personal task manager to replace Todoist. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- `chrono-node` for natural-language dates, `rrule` for recurrence. Do not
hand-roll either · both are full of calendar edge cases that look simple until
they are wrong on the last day of a month.
- Keyboard-first: `n` new, `x` complete, `/` search, `j`/`k` to move.
### Data model (create this before Phase 1)
- `tasks`: id, content, notes, project_id, priority (1-3), due_date
('YYYY-MM-DD', nullable), due_time (nullable), rrule (nullable), completed_at,
parent_id (nullable), position, created_at
- `projects`: id, name, color, archived
- `completions`: id, task_id, completed_at
Store `due_date` as a plain date string, not a timestamp. A task is due on a day,
and a timestamp will shift it across midnight for anyone who travels or observes
DST. Keep `completions` separate from `completed_at` so a recurring task has a
history rather than one ever-moving row.
### Phase 1 · Tasks and projects
Build: task CRUD, projects, priorities, notes, and completion. No parsing, no
dates yet · just the object model and a list that renders.
Done when: a task can be created, edited, completed and uncompleted; completing
writes a `completions` row; and archiving a project hides its tasks without
deleting them.
Do not build yet: parsing, recurrence, views, reminders.
### Phase 2 · Quick add with natural language
Build: the single-line parser. `call Sam tomorrow 4pm #home p2` sets content, due
date, due time, project and priority in one line. Use chrono-node for the date
portion only, after stripping `#project` and `pN` tokens · feeding those to the
parser produces confident nonsense. Show a live preview of what will be created
before the user commits, because a parser the user cannot see is a parser the
user stops trusting.
Done when: the example above parses correctly; `#home` does not end up in the
task title; "next friday" resolves to the right date near a month boundary; and
text with no date at all creates a task with no due date rather than today.
### Phase 3 · Views
Build: Today, Upcoming (next 7 days grouped by day) and per-project. Overdue
tasks pinned at the top of Today in red, and counted. Empty states that look
deliberate rather than broken.
Done when: a task due yesterday appears in Today as overdue, a task due in 3 days
appears under the right day heading in Upcoming, and Today renders correctly with
nothing due.
### Phase 4 · Recurrence
Build: recurrence with `rrule` ("every 1st", "every mon,thu"). Completing an
occurrence writes a `completions` row and schedules the next one. Decide and
document one rule: does "every 3 days" count from the due date or from the
completion date? Both are defensible and users assume different ones · pick
due-date-based for scheduled chores, offer completion-based as a per-task flag
for habits, and say which is which in the UI.
Done when: a monthly task due on the 31st behaves sanely in February, a weekly
task completed late schedules from the correct anchor per its flag, completing an
occurrence never destroys the series, and a recurring task's history is visible.
### Phase 5 · Search and subtasks
Build: full-text search across content and notes with SQLite FTS5, and one level
of subtasks (`parent_id`), where completing a parent does not silently complete
children.
Done when: search finds a phrase inside a note, results are ranked usefully, and
subtask completion state is independent and visible on the parent.
### Phase 6 · Reminders
Build: a `node-cron` loop checking each minute, pushing due tasks to a phone via
ntfy (topic in `.env`), plus a 7am digest of the day's list. Deduplicate · a task
must notify once, not every minute until completed. Survive a restart without
re-firing everything already sent.
Done when: a task due at 14:00 notifies once at 14:00, restarting the process at
14:30 does not re-notify, and the digest arrives once with the correct list.
### Phase 7 · CLI and deploy
Build: a `t add "..."` CLI hitting the same parser so capture works from any
terminal, a nightly database backup keeping 30, a systemd unit, and the README.
Done when: the CLI creates a task identical to one created in the UI, and a
restart loses nothing.
### Out of scope (and why)
- Collaboration and shared projects.
- Native mobile apps and offline sync. Phone access is ntfy notifications and the
web UI over your network · that is the trade, and it is a real downgrade from
a native app that works on a plane. Say so.
- Integrations and years of interaction polish.
### README must contain
- The quick-add syntax, with examples including one that is deliberately ambiguous.
- The recurrence anchor rule, both modes, and how to set the flag.
- ntfy setup on the phone, and where the database lives.# Agent instructions · Todoist indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22 with Express and better-sqlite3, chrono-node for dates, rrule for recurrence, node-cron and ntfy, localhost, or your own network. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · Todoist
A personal task manager on localhost: quick-add that understands 'call Sam tomorrow 4pm #home p2', Today and Upcoming views, recurring tasks that behave on the 31st, full-text search, phone reminders through ntfy, and a CLI for capture from any terminal. One SQLite file you can back up.
Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note.
## Phase 1 · Tasks and projects
The object model and a list that renders; no parsing yet.
### Steps
1. Create the project and the tables
tasks (id, content, notes, project_id, priority 1-3, due_date YYYY-MM-DD, due_time, rrule, anchor, completed_at, parent_id, position, created_at), projects (id, name, color, archived), completions (id, task_id, completed_at).
```sh
mkdir tasks && cd tasks && git init && npm init -y && npm pkg set type=module
npm install express@4 better-sqlite3@13
mkdir data && cp .env.example .env
```
2. Build task CRUD, projects, priorities, notes and completion
Completing writes a completions row. Archiving a project hides its tasks without deleting.
### Done when
- [ ] A task can be created, edited, completed and uncompleted
- [ ] Completing writes a completions row
- [ ] Archiving a project hides its tasks without deleting them
## Phase 2 · Quick add with natural language
One line becomes a task with a date, project and priority, previewed before saving.
### Steps
1. Install chrono-node and write the parser
Strip #project and pN tokens first, then hand the rest to chrono. Feeding those tokens to the parser produces confident nonsense.
```sh
npm install chrono-node@2
```
2. Show a live preview of what will be created before commit
### Done when
- [ ] 'call Sam tomorrow 4pm #home p2' parses correctly
- [ ] #home does not end up in the title
- [ ] 'next friday' resolves correctly near a month boundary
- [ ] Text with no date creates a task with no due date, not today
## Phase 3 · Views
Today, Upcoming and per-project, with overdue pinned.
### Steps
1. Build Today with overdue tasks pinned red at the top and counted
2. Build Upcoming grouped by day for seven days, and per-project views, each with a deliberate empty state
### Done when
- [ ] A task due yesterday appears in Today as overdue
- [ ] A task due in 3 days appears under the right day in Upcoming
- [ ] Today renders correctly with nothing due
## Phase 4 · Recurrence
rrule strings, the anchor rule you decided, and sane behaviour in February.
### Steps
1. Install rrule and store recurrence as RRULE strings
```sh
npm install rrule@2
```
2. Implement the anchor flag and scheduling of the next occurrence on completion
Completing writes a completions row and moves due_date to the next occurrence per the anchor.
### Done when
- [ ] A monthly task due on the 31st behaves sanely in February
- [ ] A weekly task completed late schedules from the correct anchor per its flag
- [ ] Completing never destroys the series
- [ ] History is visible
## Phase 5 · Search and subtasks
FTS5 search and one level of subtasks.
### Steps
1. Add an FTS5 table over content and notes, kept in sync by triggers
2. Add one level of subtasks via parent_id with independent completion
### Done when
- [ ] Search finds a phrase inside a note
- [ ] Completing a parent does not complete children
- [ ] Subtask state is visible on the parent
## Phase 6 · Reminders
Once per task, surviving restarts, plus a morning digest.
### Steps
1. node-cron every minute: push due tasks to ntfy once
Record notified_at on the task so a restart never re-fires.
```sh
npm install node-cron@3
curl -d 'test' ntfy.sh/$NTFY_TOPIC
```
2. Send the DIGEST_HOUR digest with the day's list
### Done when
- [ ] A task due at 14:00 notifies once at 14:00
- [ ] Restarting at 14:30 does not re-notify
- [ ] The digest arrives once with the correct list
## Phase 7 · CLI and deploy
Capture from any terminal, backups, a service.
### Steps
1. Write t add "..." using the same parser
```sh
npm link
t add "buy milk tomorrow #home"
```
2. Nightly backup keeping thirty, a systemd unit, the README
README: quick-add syntax with an ambiguous example, the anchor rule and flag, ntfy setup, where the database lives.
Files: `README.md`
### Done when
- [ ] The CLI creates a task identical to the UI
- [ ] A restart loses nothing
## Not in this build
- Collaboration and shared projects.
- Native mobile apps and offline sync; phone access is ntfy plus the web UI on your network.
- Integrations and interaction polish.
## After v1, if you want it
- A PWA shell for the phone
- Natural-language recurrence ('every other tuesday') mapped to rrule# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Bound to 127.0.0.1 or your LAN address. PORT=4810 # Required. SQLite file. DATABASE_PATH=./data/tasks.db # Required. IANA zone for reminders and the digest. TIMEZONE=Europe/Berlin # Required · secret. Your private topic from the ntfy app. NTFY_TOPIC=tasks-a8f3k2m9x1q7 # Optional. Hour of the morning digest in TIMEZONE. DIGEST_HOUR=7
# Todoist · product brief ## Problem A personal task app with projects, recurring due dates, filters, and reminders is a one-sitting/weekend build; collaboration and sync polish are the product moat. ## Product outcome A task manager you could run for your household on your network: reliable reminders, recurrence that is explainable, capture from anywhere. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - local or hosted database - date parser - notification channel - optional mobile/desktop wrappers ## Explicit non-goals for v1 - Collaboration and shared projects. - Native mobile apps and offline sync; phone access is ntfy plus the web UI on your network. - Integrations and interaction polish. - native apps - offline sync - collaboration - reminders reliability - integrations - years of UX polish ## Success criteria - Recurrence verified on the 31st, across DST, and late completion - Reminders verified once-only across a restart - One restore drill performed
# Build brief · Todoist
The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins.
Build me a personal task manager to replace Todoist. Build it in phases, in the
order below. Do not write the whole app in one pass. Finish a phase, run its
"Done when" check, fix what fails, and only then start the next phase.
### Stack (fixed, do not substitute)
- Node 22, Express and better-sqlite3, server-rendered, bound to localhost only.
- `chrono-node` for natural-language dates, `rrule` for recurrence. Do not
hand-roll either · both are full of calendar edge cases that look simple until
they are wrong on the last day of a month.
- Keyboard-first: `n` new, `x` complete, `/` search, `j`/`k` to move.
### Data model (create this before Phase 1)
- `tasks`: id, content, notes, project_id, priority (1-3), due_date
('YYYY-MM-DD', nullable), due_time (nullable), rrule (nullable), completed_at,
parent_id (nullable), position, created_at
- `projects`: id, name, color, archived
- `completions`: id, task_id, completed_at
Store `due_date` as a plain date string, not a timestamp. A task is due on a day,
and a timestamp will shift it across midnight for anyone who travels or observes
DST. Keep `completions` separate from `completed_at` so a recurring task has a
history rather than one ever-moving row.
### Phase 1 · Tasks and projects
Build: task CRUD, projects, priorities, notes, and completion. No parsing, no
dates yet · just the object model and a list that renders.
Done when: a task can be created, edited, completed and uncompleted; completing
writes a `completions` row; and archiving a project hides its tasks without
deleting them.
Do not build yet: parsing, recurrence, views, reminders.
### Phase 2 · Quick add with natural language
Build: the single-line parser. `call Sam tomorrow 4pm #home p2` sets content, due
date, due time, project and priority in one line. Use chrono-node for the date
portion only, after stripping `#project` and `pN` tokens · feeding those to the
parser produces confident nonsense. Show a live preview of what will be created
before the user commits, because a parser the user cannot see is a parser the
user stops trusting.
Done when: the example above parses correctly; `#home` does not end up in the
task title; "next friday" resolves to the right date near a month boundary; and
text with no date at all creates a task with no due date rather than today.
### Phase 3 · Views
Build: Today, Upcoming (next 7 days grouped by day) and per-project. Overdue
tasks pinned at the top of Today in red, and counted. Empty states that look
deliberate rather than broken.
Done when: a task due yesterday appears in Today as overdue, a task due in 3 days
appears under the right day heading in Upcoming, and Today renders correctly with
nothing due.
### Phase 4 · Recurrence
Build: recurrence with `rrule` ("every 1st", "every mon,thu"). Completing an
occurrence writes a `completions` row and schedules the next one. Decide and
document one rule: does "every 3 days" count from the due date or from the
completion date? Both are defensible and users assume different ones · pick
due-date-based for scheduled chores, offer completion-based as a per-task flag
for habits, and say which is which in the UI.
Done when: a monthly task due on the 31st behaves sanely in February, a weekly
task completed late schedules from the correct anchor per its flag, completing an
occurrence never destroys the series, and a recurring task's history is visible.
### Phase 5 · Search and subtasks
Build: full-text search across content and notes with SQLite FTS5, and one level
of subtasks (`parent_id`), where completing a parent does not silently complete
children.
Done when: search finds a phrase inside a note, results are ranked usefully, and
subtask completion state is independent and visible on the parent.
### Phase 6 · Reminders
Build: a `node-cron` loop checking each minute, pushing due tasks to a phone via
ntfy (topic in `.env`), plus a 7am digest of the day's list. Deduplicate · a task
must notify once, not every minute until completed. Survive a restart without
re-firing everything already sent.
Done when: a task due at 14:00 notifies once at 14:00, restarting the process at
14:30 does not re-notify, and the digest arrives once with the correct list.
### Phase 7 · CLI and deploy
Build: a `t add "..."` CLI hitting the same parser so capture works from any
terminal, a nightly database backup keeping 30, a systemd unit, and the README.
Done when: the CLI creates a task identical to one created in the UI, and a
restart loses nothing.
### Out of scope (and why)
- Collaboration and shared projects.
- Native mobile apps and offline sync. Phone access is ntfy notifications and the
web UI over your network · that is the trade, and it is a real downgrade from
a native app that works on a plane. Say so.
- Integrations and years of interaction polish.
### README must contain
- The quick-add syntax, with examples including one that is deliberately ambiguous.
- The recurrence anchor rule, both modes, and how to set the flag.
- ntfy setup on the phone, and where the database lives.# Architecture · Todoist ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22 with Express and better-sqlite3 | views, forms and a scheduler in one process | | Parsing | chrono-node for dates, rrule for recurrence | calendar edge cases look simple until they are wrong on the last day of a month | | Reminders | node-cron and ntfy | a phone notification with no app to build | | Hosting | localhost, or your own network | your tasks, your machine | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Store | tasks, projects, completions, FTS | The core | | Parser | quick-add tokens and chrono | Any parser producing the same fields | | Recurrence | rrule and anchors | Rules only | | Notify | node-cron and ntfy | Any push channel behind notify() | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Bound to 127.0.0.1 or your LAN address. - `DATABASE_PATH` · required · SQLite file. - `TIMEZONE` · required · IANA zone for reminders and the digest. - `NTFY_TOPIC` · required, secret · Your private topic from the ntfy app. - `DIGEST_HOUR` · optional · Hour of the morning digest in TIMEZONE. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Todoist product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22 with Express and better-sqlite3, chrono-node for dates, rrule for recurrence, node-cron and ntfy, localhost, or your own network. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · Todoist Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Tasks and projects The object model and a list that renders; no parsing yet. ### Steps 1. Create the project and the tables tasks (id, content, notes, project_id, priority 1-3, due_date YYYY-MM-DD, due_time, rrule, anchor, completed_at, parent_id, position, created_at), projects (id, name, color, archived), completions (id, task_id, completed_at). ```sh mkdir tasks && cd tasks && git init && npm init -y && npm pkg set type=module npm install express@4 better-sqlite3@13 mkdir data && cp .env.example .env ``` 2. Build task CRUD, projects, priorities, notes and completion Completing writes a completions row. Archiving a project hides its tasks without deleting. ### Done when - [ ] A task can be created, edited, completed and uncompleted - [ ] Completing writes a completions row - [ ] Archiving a project hides its tasks without deleting them ## M2 · Quick add with natural language One line becomes a task with a date, project and priority, previewed before saving. ### Steps 1. Install chrono-node and write the parser Strip #project and pN tokens first, then hand the rest to chrono. Feeding those tokens to the parser produces confident nonsense. ```sh npm install chrono-node@2 ``` 2. Show a live preview of what will be created before commit ### Done when - [ ] 'call Sam tomorrow 4pm #home p2' parses correctly - [ ] #home does not end up in the title - [ ] 'next friday' resolves correctly near a month boundary - [ ] Text with no date creates a task with no due date, not today ## M3 · Views Today, Upcoming and per-project, with overdue pinned. ### Steps 1. Build Today with overdue tasks pinned red at the top and counted 2. Build Upcoming grouped by day for seven days, and per-project views, each with a deliberate empty state ### Done when - [ ] A task due yesterday appears in Today as overdue - [ ] A task due in 3 days appears under the right day in Upcoming - [ ] Today renders correctly with nothing due ## M4 · Recurrence rrule strings, the anchor rule you decided, and sane behaviour in February. ### Steps 1. Install rrule and store recurrence as RRULE strings ```sh npm install rrule@2 ``` 2. Implement the anchor flag and scheduling of the next occurrence on completion Completing writes a completions row and moves due_date to the next occurrence per the anchor. ### Done when - [ ] A monthly task due on the 31st behaves sanely in February - [ ] A weekly task completed late schedules from the correct anchor per its flag - [ ] Completing never destroys the series - [ ] History is visible ## M5 · Search and subtasks FTS5 search and one level of subtasks. ### Steps 1. Add an FTS5 table over content and notes, kept in sync by triggers 2. Add one level of subtasks via parent_id with independent completion ### Done when - [ ] Search finds a phrase inside a note - [ ] Completing a parent does not complete children - [ ] Subtask state is visible on the parent ## M6 · Reminders Once per task, surviving restarts, plus a morning digest. ### Steps 1. node-cron every minute: push due tasks to ntfy once Record notified_at on the task so a restart never re-fires. ```sh npm install node-cron@3 curl -d 'test' ntfy.sh/$NTFY_TOPIC ``` 2. Send the DIGEST_HOUR digest with the day's list ### Done when - [ ] A task due at 14:00 notifies once at 14:00 - [ ] Restarting at 14:30 does not re-notify - [ ] The digest arrives once with the correct list ## M7 · CLI and deploy Capture from any terminal, backups, a service. ### Steps 1. Write t add "..." using the same parser ```sh npm link t add "buy milk tomorrow #home" ``` 2. Nightly backup keeping thirty, a systemd unit, the README README: quick-add syntax with an ambiguous example, the anchor rule and flag, ntfy setup, where the database lives. Files: `README.md` ### Done when - [ ] The CLI creates a task identical to the UI - [ ] A restart loses nothing ## M8 · Operate it like a product (production only) Only for the product-builder path: know when the task server is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443
# Operations · Todoist ## Backup SQLite .backup nightly. ## Restore Copy back; check Today. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Uptime on /healthz; a missed digest is the alarm. ## Incident checklist If the ntfy topic leaks, pick a new one; nothing else is exposed. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Recurrence verified on the 31st, across DST, and late completion - [ ] Reminders verified once-only across a restart - [ ] One restore drill performed ## Launch constraint Do not market omitted Todoist capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Bound to 127.0.0.1 or your LAN address. PORT=4810 # Required. SQLite file. DATABASE_PATH=./data/tasks.db # Required. IANA zone for reminders and the digest. TIMEZONE=Europe/Berlin # Required · secret. Your private topic from the ntfy app. NTFY_TOPIC=tasks-a8f3k2m9x1q7 # Optional. Hour of the morning digest in TIMEZONE. DIGEST_HOUR=7
$ choose a build depth, inspect the files, then open the complete pack in your agent
They pay because task capture must be fast everywhere and reminders cannot miss.
xnative apps
xoffline sync
xcollaboration
xreminders reliability
xintegrations
xyears of UX polish
Don't feel like building it? These folks already made it free.
all 4 free alternatives to Todoist →· no votes, no pay-to-list · just what's real
Todoist pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| beginner | $0/user | $0/user | 5 personal projects; 3 filters; 1 week of activity history; 5 MB files; 5 people/project. |
| pro | $7/user | $5/user | 300 personal projects; 150 filters; 100 MB files; unlimited activity history; 5 people/project. |
| business | $10/user | $8/user | 300 personal projects/member; 500 team projects; 250 people/team project; 1,000 members + 1,000 guests. |
free tier5 personal projects; 3 filters; 1 week of activity history; 5 MB files; 5 people/project.
billingmonthly + annual
hidden costsBusiness charges per member; taxes can apply.
price historyPro monthly: $5 → $7 (2025-12) · Pro annual per month: $4 → $5 (2025-12) · Business monthly: $8 → $10 (2025-12) · Business annual per month: $6 → $8 (2025-12)
verified 2026-08-14 · source ↗
Vibecode Todoist
Yes. A competent AI coding agent (Claude Code, Codex, Cursor) can build a usable personal Todoist replacement in one session with the prompt on this page. It runs on your own machine or server with no subscription.
How much does Todoist cost?
Todoist costs about $7/month (Pro, checked 2026-07-30), which is $84 per year. That's what you save by replacing it with one prompt.
What do I lose by replacing Todoist?
Honestly: native apps; offline sync; collaboration; reminders reliability; integrations; years of UX polish. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Todoist?
Yes: Super Productivity (A local task manager with timeboxing, Pomodoro, and no account begging to be created) Vikunja (A capable Todoist replacement once you accept becoming its hosting department) Microsoft To Do (A polished free task manager, paid for with a Microsoft account rather than money) All 4 curated free alternatives are at vibecodeit.com/todoist/alternatives. The prompt is for when you want it exactly your way.