- JavaScript 72.9%
- Python 24.9%
- HTML 1%
- Makefile 0.5%
- CSS 0.5%
- Other 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github | ||
| client | ||
| server | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| LICENSE | ||
| Makefile | ||
| openapi.yaml | ||
| package.json | ||
| README.md | ||
| requirements.txt | ||
Doit — Task Manager
A self-hosted task manager that runs as a PWA on phone and web. Understands natural language and syncs with Google Calendar.
Desktop
Mobile
Features
- Dashboard — productivity overview with stat cards, completion trend chart, activity heatmap, status donut, project progress bars, top tags, and streak / completion-rate insights; filterable by 7 / 30 / 90-day window
- Multi-user — admin can create users; each user has a display name, bcrypt password, and optional avatar (stored in DB, served as JPEG); role-based admin flag
- Shared projects — mark a project as shared so all users can see its tasks; per-project toggle in the project editor
- NLP scheduling — natural language → due date, time, project, tags, recurrence
- 2 views — List, Kanban board
- Group & sort — group by Status or Tags; sort by Status, Due Date, Project, Title, or Created
- Drag and drop — Kanban: drag cards between columns; Calendar: drag tasks to reschedule (disabled on touch)
- Swipe actions — on mobile, swipe a task row right to complete, left to reveal reschedule / delete
- Collapsible task groups — collapse/expand any status or tag group; state persisted, with a collapse-all toggle in the toolbar
- Completed filter — toggle to show only completed tasks (hidden by default)
- Pull to refresh — pull down on mobile to reload
- Recurring tasks — RFC 5545 RRULE format; auto-reschedules on completion
- Projects — custom icon (25 lucide icons) and color; drag to reorder in the sidebar (desktop)
- Subtasks — nested tasks with completion tracking; each subtask can carry its own due date and labels, or link to an existing task (completing the linked subtask marks the referenced task done)
- Links — attach URLs per task (GitHub, Obsidian, or any URL), auto-labeled; also extractable inline from the task title with
!<url>(rendered as link chips) - Overdue view — past-due tasks grouped by date
- Google Calendar sync — tasks with due dates sync automatically; done tasks shown in linked calendar
- ICS calendar import — import external calendars via URL or
.icsfile upload (managed in Settings) - Push notifications — ntfy or Gotify reminders; configurable per-task (timed: N minutes before due; all-day: at a set time); timezone-aware; deduplication via
reminder_sent_at - AI results — store and retrieve AI-generated content per task (model-agnostic; written by any external client via
PUT /api/tasks/<id>/ai); tasks with a stored result show an AI badge in the task detail view, and clicking it opens a modal with the rendered markdown content - Settings modal — Account tab (avatar, display name, password change), Calendars, Integrations (OTel), and Notifications
- OpenTelemetry — backend (Flask + psycopg2) and frontend (fetch + document-load) tracing; opt-in via env vars or Settings UI
- PWA — installable on iOS, Android, and macOS; share text/links from other apps straight into a pre-filled new task (share-target intake), plus app shortcuts for New Task and Today
- Mobile navigation — floating icon-only pill bottom nav (TabBar) and a swipeable full-height drawer sidebar; larger tap targets throughout
- Collapsible sidebar — full sidebar or slim icon rail (desktop); persisted preference
- Keyboard shortcuts — full shortcut set on desktop (press
?to see them) - Theme — toggle in sidebar
Quick Start
cp .env.example .env # edit credentials
make up # build and start
Open http://localhost:3000.
Pull pre-built image (no build required)
make pull # pulls ghcr.io/mvaldes14/doit:latest
Install on phone
- Open
http://<your-ip>:3000in Safari (iOS) or Chrome (Android) - Share → Add to Home Screen
Configuration
Copy .env.example to .env and set:
| Variable | Description |
|---|---|
POSTGRES_PASSWORD |
Database password (default: td) |
TD_USERNAME |
Login username |
TD_PASSWORD |
Login password (leave blank to disable auth) |
TD_API_KEY |
API key for Bearer token auth (automations) |
GCAL_CREDENTIALS_JSON |
Google service account JSON (single line) |
GCAL_CALENDAR_ID |
Calendar to sync to (default: primary) |
OTEL_EXPORTER_OTLP_ENDPOINT |
Backend gRPC OTLP endpoint (e.g. http://signoz:4317) |
OTEL_SERVICE_NAME |
Backend service name (default: doit) |
VITE_OTEL_ENDPOINT |
Frontend HTTP OTLP endpoint (e.g. http://signoz:4318); can also be set in Settings UI |
VITE_OTEL_SERVICE_NAME |
Frontend service name (default: doit-web) |
Natural Language
Type naturally in the task input — dates, times, projects, tags, and recurrence are all parsed automatically.
take out trash next monday
call dentist tuesday at 2:30pm #health
finish report by friday @urgent
standup daily at 9am
pay rent end of month
review PR in 3 days !https://github.com/org/repo/pull/42
meeting every monday and friday at 10am
fix flaky test p1 +alice
Syntax
| Syntax | Effect |
|---|---|
#projectname |
Assign to project |
@label |
Add tag |
+username |
Assign task to a user |
p1 / p2 / p3 |
Priority (high / medium / low) |
!<url> |
Strip URL from title and attach it as a link chip (auto-labeled) |
next monday, tomorrow, in 3 days |
Due date |
at 3pm, noon, EOD, morning |
Due time |
every monday, daily, every 2 weeks |
Recurrence (stored as RRULE) |
Recurrence Patterns
| Input | RRULE |
|---|---|
daily / every day |
RRULE:FREQ=DAILY |
every weekday |
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR |
every weekend |
RRULE:FREQ=WEEKLY;BYDAY=SA,SU |
every monday |
RRULE:FREQ=WEEKLY;BYDAY=MO |
every monday and friday |
RRULE:FREQ=WEEKLY;BYDAY=MO,FR |
every 2 weeks |
RRULE:FREQ=WEEKLY;INTERVAL=2 |
monthly |
RRULE:FREQ=MONTHLY |
end of month |
RRULE:FREQ=MONTHLY;BYMONTHDAY=-1 |
first monday of the month |
RRULE:FREQ=MONTHLY;BYDAY=+1MO |
yearly / annually |
RRULE:FREQ=YEARLY |
Views & Modes
| View | Description |
|---|---|
| Inbox | Unassigned tasks |
| Today | Priority focus view — today's tasks with a highlighted focus card, supporting tasks, and overdue rows |
| All Tasks | Everything |
| Overdue | Past-due tasks grouped by date |
| Calendar | Monthly calendar view |
| Projects | Tasks scoped to a project |
Each list view supports 2 display modes:
| Mode | Description |
|---|---|
| List | Grouped by status or tags; collapsible groups; show only-completed toggle; sortable |
| Board | Kanban with drag-to-reorder between columns |
Keyboard Shortcuts
Desktop only.
| Key | Action |
|---|---|
q |
Add new task |
i |
Go to Inbox |
t |
Go to Today |
o |
Go to Overdue |
c |
Go to Calendar |
l |
List view |
k |
Kanban view |
s |
Toggle sidebar collapse |
? |
Show shortcuts |
Esc |
Close modal |
Make Commands
make up # start everything (builds if needed)
make pull # pull latest image from ghcr.io and start
make down # stop
make build # force full rebuild, no cache
make restart # restart app only
make logs # tail app logs
make shell # bash into app container
make db # psql into postgres
make status # show containers + auth status
make open # open browser (macOS)
make sync-gcal # trigger Google Calendar full sync
make reset # wipe everything including DB data
API Reference
All endpoints require either a session cookie (browser login) or a Bearer token:
Authorization: Bearer <TD_API_KEY>
A full machine-readable spec is available at openapi.yaml.
NLP
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/nlp/parse |
Parse natural language into task fields |
If running locally baseurl is localhost:3000, otherwise it is your custom domain.
curl -X POST http://baseurl/api/nlp/parse \
-H "Authorization: Bearer $TD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "call dentist tuesday at 2pm #health"}'
Response:
{
"title": "call dentist",
"due_date": "2026-03-18",
"due_time": "14:00",
"project_id": "health",
"tags": [],
"recurrence": null
}
Tasks
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/tasks |
List all tasks |
GET |
/api/tasks?project_id=<id> |
Filter by project |
GET |
/api/tasks?status=todo|doing|done |
Filter by status |
GET |
/api/tasks?search=<query> |
Full-text search title + description |
GET |
/api/tasks/today |
Tasks due today |
GET |
/api/tasks/overdue |
Past-due tasks |
GET |
/api/tasks/<id> |
Get single task |
POST |
/api/tasks |
Create task |
PATCH |
/api/tasks/<id> |
Update task fields |
DELETE |
/api/tasks/<id> |
Delete task |
Task fields:
| Field | Type | Description |
|---|---|---|
title |
string | Task title |
description |
string | Notes / body |
status |
todo|doing|done |
Current status |
due_date |
YYYY-MM-DD |
Due date |
due_time |
HH:MM |
Due time (triggers timed GCal event) |
project_id |
string | Project ID (default: inbox) |
tags |
string[] | Array of tag strings |
links |
{url, label}[] |
Array of link objects |
recurrence |
string | RFC 5545 RRULE e.g. RRULE:FREQ=WEEKLY;BYDAY=MO |
recurrence_end |
YYYY-MM-DD |
Stop date for recurrence |
# Create a task
curl -X POST http://baseurl/api/tasks \
-H "Authorization: Bearer $TD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries", "due_date": "2026-03-15", "tags": ["errands"]}'
# Update status
curl -X PATCH http://baseurl/api/tasks/<id> \
-H "Authorization: Bearer $TD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status": "done"}'
# Parse NLP + create in one shot (requires jq)
curl -s -X POST http://baseurl/api/nlp/parse \
-H "Authorization: Bearer $TD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "standup tomorrow at 9am #work"}' \
| curl -X POST http://baseurl/api/tasks \
-H "Authorization: Bearer $TD_API_KEY" \
-H "Content-Type: application/json" \
-d @-
Subtasks
| Method | Endpoint | Description |
|---|---|---|
POST |
/api/tasks/<id>/subtasks |
Add subtask (title, optional due_date, due_time, labels, linked_task_id) |
PATCH |
/api/tasks/<id>/subtasks/<sid> |
Update subtask (title, due_date, due_time, completed, labels) |
DELETE |
/api/tasks/<id>/subtasks/<sid> |
Delete subtask |
Pass linked_task_id to make a subtask reference an existing task; completing that subtask sets the linked task's status to done. POST and PATCH both return the full parent task with its subtasks embedded.
Projects
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/projects |
List all projects (includes task counts), ordered by sidebar position |
POST |
/api/projects |
Create project (appended to end of order) |
PATCH |
/api/projects/<id> |
Update project |
POST |
/api/projects/reorder |
Persist sidebar order — body: {"order": ["<pid1>", "<pid2>", ...]} |
DELETE |
/api/projects/<id> |
Delete project (tasks moved to inbox) |
Project fields:
| Field | Type | Description |
|---|---|---|
name |
string | Project name |
color |
string | Hex color e.g. #7aa2f7 |
icon |
string | Lucide icon name e.g. rocket, briefcase |
Available icons: folder, home, briefcase, target, flask, book, palette, bulb, cart, dumbbell, music, plane, monitor, leaf, rocket, heart, star, zap, globe, code, camera, coffee, wrench, shield, smile
Google Calendar
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/gcal/status |
Check if GCal is connected |
POST |
/api/gcal/sync |
Trigger a full sync of all tasks with due dates |
Dashboard
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/dashboard/stats?days=30 |
Productivity stats (7/30/90d); includes counts, completion trend, activity heatmap, status breakdown, project progress, top tags, streak insights |
Users
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/users |
List all users (id, username, display_name, is_admin, has_avatar) |
GET |
/api/users/me |
Current user profile |
PATCH |
/api/users/me |
Update display name or change password (current_password + new_password) |
POST |
/api/users/me/avatar |
Upload avatar image (multipart/form-data file); resized to 50×50 JPEG |
GET |
/api/users/<id>/avatar |
Serve user avatar as JPEG |
POST |
/api/users |
Create user — admin only (username, password, optional display_name) |
Settings
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/settings |
Returns current settings (gcal_enabled, otel_frontend_endpoint, notification_type, notification_url, notification_token, reminder_enabled, reminder_minutes_before, reminder_allday_time, reminder_timezone) |
PATCH |
/api/settings |
Update settings fields |
AI Results
Store and retrieve AI-generated content attached to a task. The backend is model-agnostic — any external client can write results; the frontend reads them.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/tasks/<id>/ai |
Get stored AI result for a task |
PUT |
/api/tasks/<id>/ai |
Store (upsert) AI result for a task |
PUT body:
| Field | Type | Description |
|---|---|---|
content |
string | AI-generated text (required) |
model |
string | Model identifier e.g. claude-sonnet-4-6 (optional) |
# Store an AI result
curl -X PUT http://baseurl/api/tasks/<id>/ai \
-H "Authorization: Bearer $TD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"content": "Break this into: 1) research, 2) draft, 3) review", "model": "claude-sonnet-4-6"}'
# Retrieve it
curl http://baseurl/api/tasks/<id>/ai \
-H "Authorization: Bearer $TD_API_KEY"
GET response:
{
"content": "Break this into: 1) research, 2) draft, 3) review",
"model": "claude-sonnet-4-6",
"created_at": "2026-04-24T10:00:00",
"updated_at": "2026-04-24T10:05:00"
}
Tasks with a stored result have has_ai_result: true in their task object.
Automation Examples
iOS Shortcut — quick capture
Add tasks from anywhere on your phone via the iOS Shortcuts app:
POST baseurl/api/tasks
Headers:
Authorization: Bearer <TD_API_KEY>
Body:
title: Shortcut input
Shell alias — add task from terminal
td() {
curl -s -X POST http://baseurl/api/nlp/parse \
-H "Authorization: Bearer $TD_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"text\": \"$*\"}" \
| curl -s -X POST http://baseurl/api/tasks \
-H "Authorization: Bearer $TD_API_KEY" \
-H "Content-Type: application/json" \
-d @-
echo "Added: $*"
}
# Usage:
td buy milk tomorrow #errands
td standup daily at 9am #work
Query overdue tasks
curl -s http://baseurl/api/tasks/overdue \
-H "Authorization: Bearer $TD_API_KEY" \
| jq '[.[] | {id, title, due_date, status}]'
Stack
- Backend — Python 3.12 + Flask + PostgreSQL
- Frontend — React 19 + Vite + Tailwind CSS
- NLP —
dateparser+python-dateutil(RRULE) - AI — model-agnostic result storage per task; any client writes via
PUT /api/tasks/<id>/ai - Observability — OpenTelemetry SDK (backend: Flask + psycopg2; frontend: fetch + document-load); opt-in via env vars or Settings UI
- Theme — TailwindCSS dark/light
- Auth — Session-based with PostgreSQL storage; optional Bearer API key
- Deployment — Docker Compose; data persisted in
./data/postgres/
CI / Deployment
Every push to main builds and pushes a multi-arch image (amd64 + arm64) to ghcr.io/mvaldes14/doit:latest via GitHub Actions.
Development happens on dev — merge to main to trigger a release.
