pixari/opentodo

Your tasks, your server. An open-source, local-first task manager you can host yourself. Everything you expect from a commercial task app, without the subscription or the lock-in.

Go

3

455 commits

updated Oct 12, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built an open-source task manager you host yourself, because I was tired of paying monthly for Todoist (r/coolgithubprojects)

I built an open-source task manager you host yourself, because I was tired of paying monthly for Todoist For years I paid a subscription for a task app: Todoist It was fine, but my lists lived on someone else's server, the useful features were behind the paid plan, and moving away meant losing my…

0

Oct 12, 2026

README

OpenTodo logo

OpenTodo

Your tasks, your server.
An open-source, local-first task manager you can host yourself. Everything you expect from a commercial task app, without the subscription or the lock-in.

License: AGPL-3.0 CI Latest release Container image Docker image size Spec-driven with OpenSpec

▶ Try the demo (no sign-up) · Install in 2 minutes · Read the docs


Quick add in plain language with date, priority and project chips, a card moved across a project board, then a task added offline that syncs on reconnect

Try it in 30 seconds

  • No install: open the demo and press Try the demo. You get a private copy with sample data, deleted after a few hours.

  • On your machine or server:

    docker run -d -p 8080:8080 -v opentodo-data:/data ghcr.io/pixari/opentodo:0.3
    

    Open http://localhost:8080; the first visit creates your account. No configuration needed. Coolify, Compose and plain binaries are in Quick start.

If OpenTodo looks useful to you, a ⭐ on GitHub helps other people find it.

Today view with overdue and due-today tasks

Project view with the task detail panel Mobile dark mode

Day view: timed tasks as blocks sized by their estimate, overlapping blocks side by side, the unscheduled lane and the planned total against the day length

Why OpenTodo

  • No subscription, no lock-in. Free software under the AGPL. Nobody can paywall your task list or shut it down with the company.
  • Works offline, on every device. Each phone and computer keeps a full copy of your data (local-first), so the app works with no connection at all. The server syncs, backs up and shares; it is never in the way of an edit.
  • Your data, one folder. Everything lives in one /data volume around a single SQLite file. Export to JSON at any time, import it (or a CSV, or another app's export) on any instance, and schedule encrypted backups to disk, S3 or WebDAV.
  • Tiny footprint. One container, one static binary of about 23 MB with the web app inside, around 20 MB of memory when idle. Runs on a Raspberry Pi or the cheapest VPS. No database server, no Redis, no mandatory configuration.
  • Private by design. No telemetry, no analytics, no phone-home, no third-party scripts or fonts. The server only contacts what you configure (see Privacy).
  • Bring your own AI, if you want it. Plan and change tasks by talking to the assistant in the app (Anthropic, OpenAI, Gemini or a model on your own network), connect the assistant you already use over MCP, or let a coding agent drive the ot CLI. Off by default, every change recorded and undoable, no AI vendor built in. See Use AI with OpenTodo.
  • Keyboard-first. q to capture, j/k to move, c to complete, 1–4 for priority, g t for Today, ? for the full list, and optional Vim keybindings.
  • Built in the open with specs. Every feature starts as an OpenSpec change under openspec/, so behaviour is documented before it is built.

Status

OpenTodo is at 0.3: every feature on the roadmap is in, covered by automated tests, but some integrations still need real-world testing (CalDAV with specific apps, single sign-on providers, real phones). If you try it, tell us how it went in the call for testers.

How it's built: every feature starts as a written spec (OpenSpec), and much of the code was written with AI coding agents working from those specs. A strict CI checks every change: Go and web tests, spec validation, a phone-layout check in Chromium and WebKit, and accessibility checks. Everything is reviewed before it's merged, and the full history is public.

Features

The highlights: quick add in plain language, projects with sections and boards, recurring tasks, reminders with push, filters and saved views, time blocking, shared projects with comments, CalDAV, webhooks, a REST API and CLI, passkeys, two-factor and single sign-on, optional end-to-end encryption.

Full feature list

Capture

  • Quick add from anywhere (q), with the view's context as defaults: today in Today, the current project in a project.
  • Natural language in six languages, offline: Call Sarah next Wed 3pm #work @call p1 +30m sets the date, time, project, label, priority and estimate, with dismissible preview chips. English, German, French, Italian, Spanish and Portuguese. Syntax
  • Voice capture (opt-in per device): dictate into quick add with the microphone button or Shift+V; "hashtag home priority two" becomes #home p2. Uses the browser's own speech recognition; the server never receives audio. Voice
  • Share sheet, bookmarklet and browser extension: send a page from the phone's share menu, any desktop browser or the Chromium/Firefox extension in extension/ to a prefilled capture screen. Works offline, holds no token. Capture
  • Email to task: forward a mail to your secret address (todo+…@example.com) and it lands in the Inbox. Optional; the operator points OpenTodo at one IMAP mailbox. Email
  • Command line: ot add "Call the plumber" --due tomorrow, ot today, --json for scripts. CLI

Organisation and views

  • Inbox, Today, Upcoming, Completed and projects with colours, nested to any depth, with rolled-up counts.
  • Subtasks to any depth with done/total progress; Tab and Shift+Tab indent and outdent.
  • Sections and Kanban board per project (v toggles list and board), drag and drop on mouse and touch, or Shift+←/→.
  • Manual ordering by drag and drop or Alt+↑/↓, convergent across devices.
  • Labels across projects with colours, a type-ahead picker (L), per-label views and merging of duplicates.
  • Filters: one-line queries such as @work & due <= tomorrow & p1, evaluated on the device, live and offline. Filters
  • Saved views and dashboards: pin a filter to the sidebar with live counts, choose grouping and sort, combine up to six views on one dashboard. Saving views
  • Rich-text notes with headings, lists, check lists, links and Markdown shortcuts; edits from several devices or people merge character by character, offline too. Scripts and integrations keep reading the note as Markdown.
  • Trash and history: restore deleted tasks and projects, browse each item's change history with "restore this value", empty the Trash for good; old items are purged after 30 days. Projects whose tasks are all done are archived automatically. Purge
  • Vim keybindings (opt-in per device): modes, counts, gg/G, dd and a : command line.

Time

  • Floating due dates with optional time, the same day and time wherever you are; a timed task can be fixed to a time zone (z) for meetings and flights. See Floating vs fixed times.
  • Recurring tasks with rules you can read: every weekday, every last weekday, every 3 days after completion, until 2026-12-31, for 10 times; skip or detach one occurrence. Recurrence
  • Reminders: any number per task, relative to the due time or at a fixed moment, with snooze and dismiss. In the app offline, and as web push to devices that opt in, with no vendor account. Reminders
  • Time blocking: duration estimates (e), Day and Week calendars (g d, g w) with a 15-minute grid, drag to schedule or resize, planned hours against your day length, full keyboard control.

Collaboration

  • Shared projects with the accounts on your server as viewer, editor or admin (Shift+S); members edit offline and converge like your own devices; roles are enforced by the server. Sharing
  • Assignees (Shift+A), an Assigned to me view and in-app notifications. Assignment
  • Threaded Markdown comments with @mentions, and a per-task and per-project activity log derived from the sync history. Comments
  • Activity feed (g h) across every shared project, with unread counts that sync between your own devices only. Activity feed
  • Invitations and user administration: single-use invitation links, admin and member roles, deactivation, reset links you send yourself, owner transfer. Adding people

Integrations and data

  • REST API and API tokens (full or read-only) that write through the same op log as the app. API
  • Calendar apps (CalDAV): two-way sync with Apple Reminders, Thunderbird and Android tasks apps via DAVx⁵, with per-device app passwords. Off until the owner enables it. CalDAV
  • AI assistant in the app: plan and change your tasks by talking (Shift+I), with Anthropic (Claude), OpenAI, Gemini or a model on your own network, using your own key. Off until the owner enables it; each person accepts what is sent first; every change is proposed for review and undone in one step; conversations stay on the device. AI assistant
  • AI assistants (MCP): a read-only Model Context Protocol endpoint so an assistant can answer "what is on my plate today?". Off until the owner enables it. MCP
  • Webhooks on task and project events for Home Assistant, n8n or a Telegram relay: signed, retried, logged, with private-network protection. Webhooks
  • Import the app's own export, CSV or other task managers' JSON exports, with a preview, duplicate detection and undo. Import
  • Export everything as JSON from Settings or GET /api/v1/export.
  • Scheduled backups of the whole server to a folder, S3-compatible storage or WebDAV, with retention, optional encryption and opentodo restore. Backups

Security and privacy

  • Closed sign-up by default after the owner account is created; argon2id passwords, HttpOnly sessions, strict origin checks, CSP, rate limits.
  • Passkeys (WebAuthn) with browser autofill; needs an https OPENTODO_BASE_URL.
  • Two-factor authentication (TOTP) with recovery codes, "remember this device", an owner policy to require it, and step-up confirmation before sensitive changes.
  • Single sign-on with your own OpenID Connect provider (Authentik, Keycloak, Authelia, Zitadel…), including an SSO-only mode with owner break-glass. SSO
  • Audit log of sign-ins, credentials, invitations, exports and setting changes; never task content, client addresses only if the owner opts in. What is logged
  • Optional end-to-end encryption per account: titles, notes, names and comments are encrypted on your devices, so the server, backups and integrations hold only ciphertext. Threat model

Everywhere

  • Offline and installable: every change is applied locally first and queued; the app loads and works with no network.
  • Live sync: field-level, conflict-free sync across devices with no "pick a version" dialogs; a change shows up on your other devices within a second or two, with polling as the fallback. Sync protocol

Floating vs fixed times

A due date such as "pay rent on the 1st" or "call mum Sunday 18:00" is floating: it means that calendar day and clock time wherever you are, so it never moves when you travel. That is the default for every task.

Some times are instants: a stand-up at 09:00 Berlin time, a flight that departs at 14:35 in Lisbon. Open the task, set a time and tick Fixed to time zone (or press z): the time is pinned to your current zone. A device in Los Angeles then shows the stand-up at 00:00 with 09:00 Berlin as a hint, and Today, Upcoming, sorting, filters, reminders and the calendar all use the converted time, across daylight-saving changes. The API takes the same shape: "due":{"date":"2026-10-11","time":"09:00","tz":"Europe/Berlin"}.

Quick start

No environment variable is required. Open the app, create the owner account, and you are done; everything else is optional.

Docker

docker run -d --name opentodo -p 8080:8080 -v opentodo-data:/data \
  ghcr.io/pixari/opentodo:0.3

Open http://localhost:8080 and create the owner account. Behind a reverse proxy, add -e OPENTODO_BASE_URL=https://todo.example.com (needed for Secure cookies, passkeys and web push). The image is published for linux/amd64 and linux/arm64. :0.2 follows the 0.2 releases; :latest follows the development branch. Details: docs/self-hosting/docker.md.

Docker Compose

git clone https://github.com/pixari/opentodo.git
cd opentodo
cp .env.example .env          # optional, every value has a default
docker compose up -d

Data is kept in the opentodo-data volume.

Coolify

  1. New Resource → Public Repository, URL https://github.com/pixari/opentodo, build pack Docker Compose.
  2. Assign a domain to the opentodo service; Coolify provisions HTTPS and passes the URL to the app.
  3. Deploy, open the domain, create the owner account.

Or paste the one-click template deploy/coolify/opentodo.yaml as a Docker Compose resource (pending in Coolify's built-in service list). Details, including building from your fork: docs/self-hosting/coolify.md.

Single binary

Download opentodo-<os>-<arch> from the latest release (Linux, macOS and Windows; check it with SHA256SUMS), or build it yourself:

chmod +x opentodo-linux-amd64 && ./opentodo-linux-amd64   # serves on :8080, data in ./data
# or from source (needs Go 1.26+ and Node 22+):
make build && ./bin/opentodo

The binary is fully static (CGO_ENABLED=0, pure-Go SQLite) and also offers opentodo restore, opentodo mfa-reset, opentodo reset-owner-password, opentodo healthcheck and opentodo version.

Command line client

ot talks to any OpenTodo server over the REST API with a token it creates at login. It is a separate static binary (release assets for Linux, macOS and Windows, or go install github.com/pixari/opentodo/cmd/ot@latest) and is not part of the container image.

ot login https://todo.example.com
ot add "Call the plumber" --project Home --due tomorrow --time 09:00 --priority 2
ot today
ot done 8c2a                  # full id or unique prefix
ot list --json | jq '.[].title'

Full reference: docs/cli.md.

Configuration

Every setting is an optional environment variable with a safe default; an invalid value stops startup with a message naming the variable. The container image sets OPENTODO_ADDR=:8080 and OPENTODO_DATA_DIR=/data. See .env.example for a commented template.

VariableDefaultPurpose
OPENTODO_ADDR:8080Listen address
OPENTODO_DATA_DIR./data (binary), /data (image)Directory for the SQLite database, secrets, the VAPID key and local backups
OPENTODO_BASE_URLunset (inferred from requests)Public URL. With https: Secure cookies, passkeys and web push; also used for strict origin checks and in invitation, reset and CalDAV links
OPENTODO_ALLOW_SIGNUPfalseLet anyone create an account after the owner exists. Prefer invitations
OPENTODO_SESSION_DAYS90Browser session lifetime in days
OPENTODO_LOG_LEVELinfodebug, info, warn or error
OPENTODO_IMPORT_MAX_MB20Largest import file, 1–200
OPENTODO_TRASH_RETENTION_DAYS30Days a deleted task or project stays in the Trash before it is purged, 0–3650; 0 never purges automatically. See Purge
OPENTODO_DEMOunset (off)true turns a dedicated instance into a public demo: a private, seeded sandbox per visitor, deleted after 2 h of inactivity, after 24 h and every night at 03:00 UTC; abusable features refused with 403 demo_restricted
OPENTODO_DEMO_MAX_SANDBOXES, OPENTODO_DEMO_MAX_DB_MB500, 1024Optional demo tuning: live sandbox cap and the database size above which no new sandbox is created
OPENTODO_TRUSTED_PROXIES—Optional: proxies in front of your reverse proxy whose X-Forwarded-For hops are skipped for rate limits and the audit log (IPs, CIDRs, cloudflare)
OPENTODO_PUSHonoff disables web push for reminders: no requests to push services; reminders still show in the app
OPENTODO_WEBHOOKS_ALLOW_PRIVATEunsettrue/false presets the owner's "allow private network targets" webhook policy on first start; afterwards Settings decides
OPENTODO_MCPunset (off)true presets the AI assistants (MCP) endpoint on first start; afterwards Settings → AI → AI assistants decides
OPENTODO_MCP_WRITESunset (off)true presets "AI assistants may make changes" on first start; each user must still allow it and use an agent token. Afterwards Settings → AI → AI assistants decides
OPENTODO_CALDAVunset (off)true presets the CalDAV endpoint on first start; afterwards Settings → Integrations → Calendar apps decides
OPENTODO_OIDC_ISSUER, _CLIENT_ID, _CLIENT_SECRET, _DISPLAY_NAME, _SSO_ONLYunsetOptional single sign-on settings; each one set overrides and locks that field in Settings → Administration → Single sign-on. The issuer must be https
OPENTODO_EMAIL_ADDRESS, _IMAP_HOST, _IMAP_USER, _IMAP_PASSWORD (or _IMAP_PASSWORD_FILE)unset (off)Email to task: all four together turn it on; setting only some stops startup
OPENTODO_EMAIL_IMAP_MAILBOX, _POLL_SECONDS, _PROCESSEDINBOX, 60, move:OpenTodo/ProcessedEmail to task tuning (minimum poll 15 s; _PROCESSED may be delete)
OPENTODO_BACKUP_*unsetOptional presets for scheduled backups (DESTINATION, SCHEDULE, TIME, WEEKDAY, KEEP, MAX_AGE_DAYS, COMPRESS, LOCAL_DIR, S3_*, WEBDAV_*, PASSPHRASE/PASSPHRASE_FILE), applied once; Settings → Data → Backups wins afterwards

OPENTODO_PORT in .env only changes the host port published by docker-compose.yml. The ot client reads its own variables (OPENTODO_URL, OPENTODO_TOKEN, OPENTODO_PROFILE, OPENTODO_CONFIG), see docs/cli.md.

Adding people

Sign-up stays closed after the owner account exists. To add a partner or a colleague, open Settings → Administration → Members, optionally enter their email, pick an expiry (1 hour to 30 days, default 7 days) and create an invitation. Copy the link (it is shown once) and send it however you like; OpenTodo sends no email. The link creates exactly one member account and then stops working; you can revoke a pending link at any time. Admins can create invitations too.

Manage accounts afterwards in Settings → Administration → Users: promote admins, deactivate and reactivate, issue password reset links, delete accounts, transfer ownership (docs/self-hosting/docker.md). If the owner forgets their password, opentodo reset-owner-password on the host prints a reset link. With single sign-on configured, people can also sign in through your identity provider. Set OPENTODO_BASE_URL so links point at your public URL. API details: docs/api.md.

How it works

┌───────────────────────── device ─────────────────────────┐      ┌──────────── server ────────────┐
│ React PWA  ──►  IndexedDB (source of truth)              │      │ Go binary + SQLite             │
│                 │ every edit = field ops {ts, device}    │ sync │  ops log (append-only)         │
│                 └► outbox ──────────────────────────────►├─────►│  fields (last-writer-wins)     │
│                 ◄─ ops from other devices ◄──────────────┤◄─────┤  REST, CalDAV, email, import   │
└──────────────────────────────────────────────────────────┘      │  all write through ops         │
                                                                  └────────────────────────────────┘
  • Every change is a small operation that sets one field of one entity, stamped with a hybrid logical clock and the device id.
  • Clients apply ops locally first, then push the outbox and pull what other devices wrote. The same per-field last-writer-wins rule runs on every replica, so all devices converge no matter the order ops arrive in. Rich-text notes are mergeable documents, so concurrent edits to one note combine instead of replacing each other.
  • Each op belongs to a sharing scope (your personal data, or one shared project); the server checks the author's role when it receives an op.
  • Connected devices keep a lightweight server-sent event stream open; when the server records new ops it tells the other devices "sync now" (no data on the wire). If the stream is unavailable the app polls.
  • The REST API, CalDAV, email capture and imports write through the same log, so a task created with curl shows up on your phone right away.
  • Deletes are tombstones, which keeps sync correct and powers the Trash.

More in docs/architecture.md and docs/sync.md.

Documentation

Read the docs with screenshots and search at opentodo.pixari.dev/docs, or in the app under Help (offline, matching your server's version). The same pages are Markdown in docs/:

Development

make dev        # Go API on :8080 + Vite with hot reload on :5173
make check      # vet, typecheck, Go + web + extension tests, docs check, OpenSpec validation (what CI runs)
make docs       # regenerate the docs website and in-app help from docs/

Repository layout:

cmd/opentodo/        server entry point (serve, restore, mfa-reset, reset-owner-password, healthcheck, version)
cmd/ot/              command-line client entry point (logic in internal/cli)
internal/            Go packages: store, sync, auth, httpapi, caldav, mcp, email, push, backup, … (see docs/architecture.md)
web/                 React 19 + Vite + Tailwind PWA; builds into internal/web/dist
extension/           browser capture extension (Manifest V3, plain JS)
openspec/            specs (live behaviour contracts) and changes (proposals in flight and archived)
docs/                user, operator and protocol documentation

Working with specs

This repository uses OpenSpec. The current behaviour of the product is described in openspec/specs/<capability>/spec.md; each new feature is a change folder with a proposal, delta specs, a design and a task list. With Claude Code or any supported assistant:

/opsx:propose "natural language quick add"   # plan a change
/opsx:apply                                  # implement it
/opsx:archive                                # merge its specs into the live ones

Validate everything with npx @fission-ai/openspec validate --all --strict (also part of make check and CI).

Privacy

The web app makes no network requests except to your own server: no analytics, crash reporters, update pings or third-party fonts. The server makes outbound requests only for features you configure:

  • web push to the push service a device's browser chose (Google, Mozilla, Apple), end-to-end encrypted and carrying only ids unless the device opts in to titles (OPENTODO_PUSH=off turns push off);
  • webhook URLs you add;
  • backup destinations (S3 or WebDAV) you configure;
  • the single sign-on issuer you configure;
  • the IMAP mailbox you configure for email to task;
  • the model provider you enable for the in-app assistant (off by default), only after a person has accepted what is sent and only while they wait for an answer.

The CalDAV and MCP endpoints only answer requests; they never call out. The audit log records security events (sign-ins, tokens, invitations, exports), never task content, and stores client addresses only if the owner turns that on; see SECURITY.md. With optional end-to-end encryption the server holds only ciphertext for task content.

Contributing

Issues and pull requests are welcome. Please read CONTRIBUTING.md: changes that alter behaviour go through an OpenSpec change first. Security issues go to the address in SECURITY.md.

License

AGPL-3.0-only. You can run, study, modify and share OpenTodo freely. If you offer a modified version as a network service, you must offer its source under the same license. That is what keeps a self-hosted project from being quietly turned back into a subscription.

chores
productivity
todo

pixari/opentodo

Your tasks, your server. An open-source, local-first task manager you can host yourself. Everything you expect from a commercial task app, without the subscription or the lock-in.

Go

3

455 commits

updated Oct 12, 2026

See the code

See what people are saying

SourceMessageScoreDate

I built an open-source task manager you host yourself, because I was tired of paying monthly for Todoist (r/coolgithubprojects)

I built an open-source task manager you host yourself, because I was tired of paying monthly for Todoist For years I paid a subscription for a task app: Todoist It was fine, but my lists lived on someone else's server, the useful features were behind the paid plan, and moving away meant losing my…

0

Oct 12, 2026

README

OpenTodo logo

OpenTodo

Your tasks, your server.
An open-source, local-first task manager you can host yourself. Everything you expect from a commercial task app, without the subscription or the lock-in.

License: AGPL-3.0 CI Latest release Container image Docker image size Spec-driven with OpenSpec

▶ Try the demo (no sign-up) · Install in 2 minutes · Read the docs


Quick add in plain language with date, priority and project chips, a card moved across a project board, then a task added offline that syncs on reconnect

Try it in 30 seconds

  • No install: open the demo and press Try the demo. You get a private copy with sample data, deleted after a few hours.

  • On your machine or server:

    docker run -d -p 8080:8080 -v opentodo-data:/data ghcr.io/pixari/opentodo:0.3
    

    Open http://localhost:8080; the first visit creates your account. No configuration needed. Coolify, Compose and plain binaries are in Quick start.

If OpenTodo looks useful to you, a ⭐ on GitHub helps other people find it.

Today view with overdue and due-today tasks

Project view with the task detail panel Mobile dark mode

Day view: timed tasks as blocks sized by their estimate, overlapping blocks side by side, the unscheduled lane and the planned total against the day length

Why OpenTodo

  • No subscription, no lock-in. Free software under the AGPL. Nobody can paywall your task list or shut it down with the company.
  • Works offline, on every device. Each phone and computer keeps a full copy of your data (local-first), so the app works with no connection at all. The server syncs, backs up and shares; it is never in the way of an edit.
  • Your data, one folder. Everything lives in one /data volume around a single SQLite file. Export to JSON at any time, import it (or a CSV, or another app's export) on any instance, and schedule encrypted backups to disk, S3 or WebDAV.
  • Tiny footprint. One container, one static binary of about 23 MB with the web app inside, around 20 MB of memory when idle. Runs on a Raspberry Pi or the cheapest VPS. No database server, no Redis, no mandatory configuration.
  • Private by design. No telemetry, no analytics, no phone-home, no third-party scripts or fonts. The server only contacts what you configure (see Privacy).
  • Bring your own AI, if you want it. Plan and change tasks by talking to the assistant in the app (Anthropic, OpenAI, Gemini or a model on your own network), connect the assistant you already use over MCP, or let a coding agent drive the ot CLI. Off by default, every change recorded and undoable, no AI vendor built in. See Use AI with OpenTodo.
  • Keyboard-first. q to capture, j/k to move, c to complete, 1–4 for priority, g t for Today, ? for the full list, and optional Vim keybindings.
  • Built in the open with specs. Every feature starts as an OpenSpec change under openspec/, so behaviour is documented before it is built.

Status

OpenTodo is at 0.3: every feature on the roadmap is in, covered by automated tests, but some integrations still need real-world testing (CalDAV with specific apps, single sign-on providers, real phones). If you try it, tell us how it went in the call for testers.

How it's built: every feature starts as a written spec (OpenSpec), and much of the code was written with AI coding agents working from those specs. A strict CI checks every change: Go and web tests, spec validation, a phone-layout check in Chromium and WebKit, and accessibility checks. Everything is reviewed before it's merged, and the full history is public.

Features

The highlights: quick add in plain language, projects with sections and boards, recurring tasks, reminders with push, filters and saved views, time blocking, shared projects with comments, CalDAV, webhooks, a REST API and CLI, passkeys, two-factor and single sign-on, optional end-to-end encryption.

Full feature list

Capture

  • Quick add from anywhere (q), with the view's context as defaults: today in Today, the current project in a project.
  • Natural language in six languages, offline: Call Sarah next Wed 3pm #work @call p1 +30m sets the date, time, project, label, priority and estimate, with dismissible preview chips. English, German, French, Italian, Spanish and Portuguese. Syntax
  • Voice capture (opt-in per device): dictate into quick add with the microphone button or Shift+V; "hashtag home priority two" becomes #home p2. Uses the browser's own speech recognition; the server never receives audio. Voice
  • Share sheet, bookmarklet and browser extension: send a page from the phone's share menu, any desktop browser or the Chromium/Firefox extension in extension/ to a prefilled capture screen. Works offline, holds no token. Capture
  • Email to task: forward a mail to your secret address (todo+…@example.com) and it lands in the Inbox. Optional; the operator points OpenTodo at one IMAP mailbox. Email
  • Command line: ot add "Call the plumber" --due tomorrow, ot today, --json for scripts. CLI

Organisation and views

  • Inbox, Today, Upcoming, Completed and projects with colours, nested to any depth, with rolled-up counts.
  • Subtasks to any depth with done/total progress; Tab and Shift+Tab indent and outdent.
  • Sections and Kanban board per project (v toggles list and board), drag and drop on mouse and touch, or Shift+←/→.
  • Manual ordering by drag and drop or Alt+↑/↓, convergent across devices.
  • Labels across projects with colours, a type-ahead picker (L), per-label views and merging of duplicates.
  • Filters: one-line queries such as @work & due <= tomorrow & p1, evaluated on the device, live and offline. Filters
  • Saved views and dashboards: pin a filter to the sidebar with live counts, choose grouping and sort, combine up to six views on one dashboard. Saving views
  • Rich-text notes with headings, lists, check lists, links and Markdown shortcuts; edits from several devices or people merge character by character, offline too. Scripts and integrations keep reading the note as Markdown.
  • Trash and history: restore deleted tasks and projects, browse each item's change history with "restore this value", empty the Trash for good; old items are purged after 30 days. Projects whose tasks are all done are archived automatically. Purge
  • Vim keybindings (opt-in per device): modes, counts, gg/G, dd and a : command line.

Time

  • Floating due dates with optional time, the same day and time wherever you are; a timed task can be fixed to a time zone (z) for meetings and flights. See Floating vs fixed times.
  • Recurring tasks with rules you can read: every weekday, every last weekday, every 3 days after completion, until 2026-12-31, for 10 times; skip or detach one occurrence. Recurrence
  • Reminders: any number per task, relative to the due time or at a fixed moment, with snooze and dismiss. In the app offline, and as web push to devices that opt in, with no vendor account. Reminders
  • Time blocking: duration estimates (e), Day and Week calendars (g d, g w) with a 15-minute grid, drag to schedule or resize, planned hours against your day length, full keyboard control.

Collaboration

  • Shared projects with the accounts on your server as viewer, editor or admin (Shift+S); members edit offline and converge like your own devices; roles are enforced by the server. Sharing
  • Assignees (Shift+A), an Assigned to me view and in-app notifications. Assignment
  • Threaded Markdown comments with @mentions, and a per-task and per-project activity log derived from the sync history. Comments
  • Activity feed (g h) across every shared project, with unread counts that sync between your own devices only. Activity feed
  • Invitations and user administration: single-use invitation links, admin and member roles, deactivation, reset links you send yourself, owner transfer. Adding people

Integrations and data

  • REST API and API tokens (full or read-only) that write through the same op log as the app. API
  • Calendar apps (CalDAV): two-way sync with Apple Reminders, Thunderbird and Android tasks apps via DAVx⁵, with per-device app passwords. Off until the owner enables it. CalDAV
  • AI assistant in the app: plan and change your tasks by talking (Shift+I), with Anthropic (Claude), OpenAI, Gemini or a model on your own network, using your own key. Off until the owner enables it; each person accepts what is sent first; every change is proposed for review and undone in one step; conversations stay on the device. AI assistant
  • AI assistants (MCP): a read-only Model Context Protocol endpoint so an assistant can answer "what is on my plate today?". Off until the owner enables it. MCP
  • Webhooks on task and project events for Home Assistant, n8n or a Telegram relay: signed, retried, logged, with private-network protection. Webhooks
  • Import the app's own export, CSV or other task managers' JSON exports, with a preview, duplicate detection and undo. Import
  • Export everything as JSON from Settings or GET /api/v1/export.
  • Scheduled backups of the whole server to a folder, S3-compatible storage or WebDAV, with retention, optional encryption and opentodo restore. Backups

Security and privacy

  • Closed sign-up by default after the owner account is created; argon2id passwords, HttpOnly sessions, strict origin checks, CSP, rate limits.
  • Passkeys (WebAuthn) with browser autofill; needs an https OPENTODO_BASE_URL.
  • Two-factor authentication (TOTP) with recovery codes, "remember this device", an owner policy to require it, and step-up confirmation before sensitive changes.
  • Single sign-on with your own OpenID Connect provider (Authentik, Keycloak, Authelia, Zitadel…), including an SSO-only mode with owner break-glass. SSO
  • Audit log of sign-ins, credentials, invitations, exports and setting changes; never task content, client addresses only if the owner opts in. What is logged
  • Optional end-to-end encryption per account: titles, notes, names and comments are encrypted on your devices, so the server, backups and integrations hold only ciphertext. Threat model

Everywhere

  • Offline and installable: every change is applied locally first and queued; the app loads and works with no network.
  • Live sync: field-level, conflict-free sync across devices with no "pick a version" dialogs; a change shows up on your other devices within a second or two, with polling as the fallback. Sync protocol

Floating vs fixed times

A due date such as "pay rent on the 1st" or "call mum Sunday 18:00" is floating: it means that calendar day and clock time wherever you are, so it never moves when you travel. That is the default for every task.

Some times are instants: a stand-up at 09:00 Berlin time, a flight that departs at 14:35 in Lisbon. Open the task, set a time and tick Fixed to time zone (or press z): the time is pinned to your current zone. A device in Los Angeles then shows the stand-up at 00:00 with 09:00 Berlin as a hint, and Today, Upcoming, sorting, filters, reminders and the calendar all use the converted time, across daylight-saving changes. The API takes the same shape: "due":{"date":"2026-10-11","time":"09:00","tz":"Europe/Berlin"}.

Quick start

No environment variable is required. Open the app, create the owner account, and you are done; everything else is optional.

Docker

docker run -d --name opentodo -p 8080:8080 -v opentodo-data:/data \
  ghcr.io/pixari/opentodo:0.3

Open http://localhost:8080 and create the owner account. Behind a reverse proxy, add -e OPENTODO_BASE_URL=https://todo.example.com (needed for Secure cookies, passkeys and web push). The image is published for linux/amd64 and linux/arm64. :0.2 follows the 0.2 releases; :latest follows the development branch. Details: docs/self-hosting/docker.md.

Docker Compose

git clone https://github.com/pixari/opentodo.git
cd opentodo
cp .env.example .env          # optional, every value has a default
docker compose up -d

Data is kept in the opentodo-data volume.

Coolify

  1. New Resource → Public Repository, URL https://github.com/pixari/opentodo, build pack Docker Compose.
  2. Assign a domain to the opentodo service; Coolify provisions HTTPS and passes the URL to the app.
  3. Deploy, open the domain, create the owner account.

Or paste the one-click template deploy/coolify/opentodo.yaml as a Docker Compose resource (pending in Coolify's built-in service list). Details, including building from your fork: docs/self-hosting/coolify.md.

Single binary

Download opentodo-<os>-<arch> from the latest release (Linux, macOS and Windows; check it with SHA256SUMS), or build it yourself:

chmod +x opentodo-linux-amd64 && ./opentodo-linux-amd64   # serves on :8080, data in ./data
# or from source (needs Go 1.26+ and Node 22+):
make build && ./bin/opentodo

The binary is fully static (CGO_ENABLED=0, pure-Go SQLite) and also offers opentodo restore, opentodo mfa-reset, opentodo reset-owner-password, opentodo healthcheck and opentodo version.

Command line client

ot talks to any OpenTodo server over the REST API with a token it creates at login. It is a separate static binary (release assets for Linux, macOS and Windows, or go install github.com/pixari/opentodo/cmd/ot@latest) and is not part of the container image.

ot login https://todo.example.com
ot add "Call the plumber" --project Home --due tomorrow --time 09:00 --priority 2
ot today
ot done 8c2a                  # full id or unique prefix
ot list --json | jq '.[].title'

Full reference: docs/cli.md.

Configuration

Every setting is an optional environment variable with a safe default; an invalid value stops startup with a message naming the variable. The container image sets OPENTODO_ADDR=:8080 and OPENTODO_DATA_DIR=/data. See .env.example for a commented template.

VariableDefaultPurpose
OPENTODO_ADDR:8080Listen address
OPENTODO_DATA_DIR./data (binary), /data (image)Directory for the SQLite database, secrets, the VAPID key and local backups
OPENTODO_BASE_URLunset (inferred from requests)Public URL. With https: Secure cookies, passkeys and web push; also used for strict origin checks and in invitation, reset and CalDAV links
OPENTODO_ALLOW_SIGNUPfalseLet anyone create an account after the owner exists. Prefer invitations
OPENTODO_SESSION_DAYS90Browser session lifetime in days
OPENTODO_LOG_LEVELinfodebug, info, warn or error
OPENTODO_IMPORT_MAX_MB20Largest import file, 1–200
OPENTODO_TRASH_RETENTION_DAYS30Days a deleted task or project stays in the Trash before it is purged, 0–3650; 0 never purges automatically. See Purge
OPENTODO_DEMOunset (off)true turns a dedicated instance into a public demo: a private, seeded sandbox per visitor, deleted after 2 h of inactivity, after 24 h and every night at 03:00 UTC; abusable features refused with 403 demo_restricted
OPENTODO_DEMO_MAX_SANDBOXES, OPENTODO_DEMO_MAX_DB_MB500, 1024Optional demo tuning: live sandbox cap and the database size above which no new sandbox is created
OPENTODO_TRUSTED_PROXIES—Optional: proxies in front of your reverse proxy whose X-Forwarded-For hops are skipped for rate limits and the audit log (IPs, CIDRs, cloudflare)
OPENTODO_PUSHonoff disables web push for reminders: no requests to push services; reminders still show in the app
OPENTODO_WEBHOOKS_ALLOW_PRIVATEunsettrue/false presets the owner's "allow private network targets" webhook policy on first start; afterwards Settings decides
OPENTODO_MCPunset (off)true presets the AI assistants (MCP) endpoint on first start; afterwards Settings → AI → AI assistants decides
OPENTODO_MCP_WRITESunset (off)true presets "AI assistants may make changes" on first start; each user must still allow it and use an agent token. Afterwards Settings → AI → AI assistants decides
OPENTODO_CALDAVunset (off)true presets the CalDAV endpoint on first start; afterwards Settings → Integrations → Calendar apps decides
OPENTODO_OIDC_ISSUER, _CLIENT_ID, _CLIENT_SECRET, _DISPLAY_NAME, _SSO_ONLYunsetOptional single sign-on settings; each one set overrides and locks that field in Settings → Administration → Single sign-on. The issuer must be https
OPENTODO_EMAIL_ADDRESS, _IMAP_HOST, _IMAP_USER, _IMAP_PASSWORD (or _IMAP_PASSWORD_FILE)unset (off)Email to task: all four together turn it on; setting only some stops startup
OPENTODO_EMAIL_IMAP_MAILBOX, _POLL_SECONDS, _PROCESSEDINBOX, 60, move:OpenTodo/ProcessedEmail to task tuning (minimum poll 15 s; _PROCESSED may be delete)
OPENTODO_BACKUP_*unsetOptional presets for scheduled backups (DESTINATION, SCHEDULE, TIME, WEEKDAY, KEEP, MAX_AGE_DAYS, COMPRESS, LOCAL_DIR, S3_*, WEBDAV_*, PASSPHRASE/PASSPHRASE_FILE), applied once; Settings → Data → Backups wins afterwards

OPENTODO_PORT in .env only changes the host port published by docker-compose.yml. The ot client reads its own variables (OPENTODO_URL, OPENTODO_TOKEN, OPENTODO_PROFILE, OPENTODO_CONFIG), see docs/cli.md.

Adding people

Sign-up stays closed after the owner account exists. To add a partner or a colleague, open Settings → Administration → Members, optionally enter their email, pick an expiry (1 hour to 30 days, default 7 days) and create an invitation. Copy the link (it is shown once) and send it however you like; OpenTodo sends no email. The link creates exactly one member account and then stops working; you can revoke a pending link at any time. Admins can create invitations too.

Manage accounts afterwards in Settings → Administration → Users: promote admins, deactivate and reactivate, issue password reset links, delete accounts, transfer ownership (docs/self-hosting/docker.md). If the owner forgets their password, opentodo reset-owner-password on the host prints a reset link. With single sign-on configured, people can also sign in through your identity provider. Set OPENTODO_BASE_URL so links point at your public URL. API details: docs/api.md.

How it works

┌───────────────────────── device ─────────────────────────┐      ┌──────────── server ────────────┐
│ React PWA  ──►  IndexedDB (source of truth)              │      │ Go binary + SQLite             │
│                 │ every edit = field ops {ts, device}    │ sync │  ops log (append-only)         │
│                 └► outbox ──────────────────────────────►├─────►│  fields (last-writer-wins)     │
│                 ◄─ ops from other devices ◄──────────────┤◄─────┤  REST, CalDAV, email, import   │
└──────────────────────────────────────────────────────────┘      │  all write through ops         │
                                                                  └────────────────────────────────┘
  • Every change is a small operation that sets one field of one entity, stamped with a hybrid logical clock and the device id.
  • Clients apply ops locally first, then push the outbox and pull what other devices wrote. The same per-field last-writer-wins rule runs on every replica, so all devices converge no matter the order ops arrive in. Rich-text notes are mergeable documents, so concurrent edits to one note combine instead of replacing each other.
  • Each op belongs to a sharing scope (your personal data, or one shared project); the server checks the author's role when it receives an op.
  • Connected devices keep a lightweight server-sent event stream open; when the server records new ops it tells the other devices "sync now" (no data on the wire). If the stream is unavailable the app polls.
  • The REST API, CalDAV, email capture and imports write through the same log, so a task created with curl shows up on your phone right away.
  • Deletes are tombstones, which keeps sync correct and powers the Trash.

More in docs/architecture.md and docs/sync.md.

Documentation

Read the docs with screenshots and search at opentodo.pixari.dev/docs, or in the app under Help (offline, matching your server's version). The same pages are Markdown in docs/:

Development

make dev        # Go API on :8080 + Vite with hot reload on :5173
make check      # vet, typecheck, Go + web + extension tests, docs check, OpenSpec validation (what CI runs)
make docs       # regenerate the docs website and in-app help from docs/

Repository layout:

cmd/opentodo/        server entry point (serve, restore, mfa-reset, reset-owner-password, healthcheck, version)
cmd/ot/              command-line client entry point (logic in internal/cli)
internal/            Go packages: store, sync, auth, httpapi, caldav, mcp, email, push, backup, … (see docs/architecture.md)
web/                 React 19 + Vite + Tailwind PWA; builds into internal/web/dist
extension/           browser capture extension (Manifest V3, plain JS)
openspec/            specs (live behaviour contracts) and changes (proposals in flight and archived)
docs/                user, operator and protocol documentation

Working with specs

This repository uses OpenSpec. The current behaviour of the product is described in openspec/specs/<capability>/spec.md; each new feature is a change folder with a proposal, delta specs, a design and a task list. With Claude Code or any supported assistant:

/opsx:propose "natural language quick add"   # plan a change
/opsx:apply                                  # implement it
/opsx:archive                                # merge its specs into the live ones

Validate everything with npx @fission-ai/openspec validate --all --strict (also part of make check and CI).

Privacy

The web app makes no network requests except to your own server: no analytics, crash reporters, update pings or third-party fonts. The server makes outbound requests only for features you configure:

  • web push to the push service a device's browser chose (Google, Mozilla, Apple), end-to-end encrypted and carrying only ids unless the device opts in to titles (OPENTODO_PUSH=off turns push off);
  • webhook URLs you add;
  • backup destinations (S3 or WebDAV) you configure;
  • the single sign-on issuer you configure;
  • the IMAP mailbox you configure for email to task;
  • the model provider you enable for the in-app assistant (off by default), only after a person has accepted what is sent and only while they wait for an answer.

The CalDAV and MCP endpoints only answer requests; they never call out. The audit log records security events (sign-ins, tokens, invitations, exports), never task content, and stores client addresses only if the owner turns that on; see SECURITY.md. With optional end-to-end encryption the server holds only ciphertext for task content.

Contributing

Issues and pull requests are welcome. Please read CONTRIBUTING.md: changes that alter behaviour go through an OpenSpec change first. Security issues go to the address in SECURITY.md.

License

AGPL-3.0-only. You can run, study, modify and share OpenTodo freely. If you offer a modified version as a network service, you must offer its source under the same license. That is what keeps a self-hosted project from being quietly turned back into a subscription.

chores
productivity
todo

Project health

Latest release

v0.2.0 · Oct 11, 2026

Releases this year

2