OpenTodo 0.1.0
Released on
The first release, and the first feature-complete version: every item on the roadmap is implemented. Each feature was planned and built as an OpenSpec change; the archived changes under openspec/changes/archive/ hold the full proposals, designs and task lists, and openspec/specs/ the resulting behaviour contracts.
Upgrade notes
- No mandatory configuration. Every new environment variable is optional and every feature that talks to the outside world is off until configured. The complete list is in the README.
- Database migrations
0001to0024are embedded and applied automatically at startup, in order, tracked byPRAGMA user_version(numbers0016–0018and0020are unused). Back up/databefore upgrading an existing instance: there are no down-migrations. Rolling back to a binary older than user administration lets deactivated accounts sign in again. - Update the server before clients. The op schema only grew (new fields and entities, nothing renamed), but an older server rejects sync batches that carry fields it does not know; those ops wait in the device's outbox until the server is upgraded. Older clients keep syncing their personal data and skip ops for entities they do not know.
- Sizes (measured for this release,
linux/amd64): server binary 16,715,938 bytes (15.9 MiB) built with-trimpath -ldflags="-s -w", web app embedded;otCLI about 6.9 MB; initial web bundle 261 KB JS + 10.7 KB CSS gzipped, with the rich-text editor (97 KB), voice recognition (4 KB) and Vim layer (4 KB) loaded on demand; idle memory about 18 MB on a fresh instance. The container image adds the distroless static base (about 2 MB) to the binary. Release assets: server andotbinaries for Linux, macOS and Windows (amd64 and arm64), the browser extension zip andSHA256SUMS; imagesghcr.io/pixari/opentodo:0.1.0and:0.1forlinux/amd64andlinux/arm64.
Added
- Go + SQLite server with the React PWA embedded, local-first storage in IndexedDB, field-level last-writer-wins op log, owner setup, sessions, API tokens, REST API that writes through the op log, JSON export, a single distroless container and a Coolify-ready compose file. Architecture, sync protocol
- Live sync: a server-sent event stream (
GET /api/v1/sync/events) wakes other devices as soon as ops are recorded; 30 s polling remains the fallback. Live updates, proxy notes - Vim keybindings, opt-in per device and loaded only when enabled.
- Natural-language quick add in English, German, French, Italian, Spanish and Portuguese: dates, times,
#project,@label,p1–p4,+30mestimates, with preview chips; parsed on the device. Quick add - Voice capture into quick add (
Shift+V), opt-in per device, using the browser's speech recognition. Voice - Share target, bookmarklet and a Manifest V3 browser extension for Chromium and Firefox (
make extension), opening a capture screen at/share. Capture - Email to task: a personal secret address per account, polled from one operator-configured IMAP mailbox (
OPENTODO_EMAIL_*). Email otcommand-line client (cmd/ot), cross-compiled for Linux, macOS and Windows and attached to releases. CLI- Nested projects and subtask trees to any depth, with deterministic, write-free cycle breaking for concurrent moves.
- Sections and a Kanban board per project; new
sectionentity and task fieldsection_id. Sections - Manual ordering by drag and drop and
Alt+↑/↓, onepositionop per move. Manual order - Labels as a synced entity referenced by id from
task.labels. Task labels - Filter query language evaluated on the device, and saved views and dashboards (
viewentity, REST/api/v1/views, validated by a Go port of the parser). Filters - Rich-text notes with collaborative editing: new task field
notes_doc(a Yjs document envelope, up to 256 KB) next todescription, which stays the note's Markdown projection for the API, export, CalDAV, MCP and webhooks. Mergeable documents - Trash with restore, revision history from the op log, permanent purge (
purgestable,purgedin sync responses), automatic purge afterOPENTODO_TRASH_RETENTION_DAYS(default 30) and automatic archiving of finished projects. Trash and history, Purge - Duration estimates (task field
estimate_minutes) and Day/Week time-blocking calendars with drag, resize and full keyboard control. - Recurring tasks: task fields
recurrenceandseries_id, one grammar implemented in Go and TypeScript against 180 shared vectors, convergent completion of the same occurrence on two offline devices. Recurrence - Reminders (
reminderentity, relative or absolute) with in-app notifications, snooze and dismiss, and web push implemented with the standard library (VAPID, RFC 8291); key pair in<data>/push_vapid.pem,OPENTODO_PUSH=offto disable. Reminders - Time-zone-fixed due times: optional
tzkey indue(IANA zone, only withtime), resolved byinternal/zonedand the web app against shared vectors. Zoned due values - Project sharing with viewer, editor, admin and owner roles: the op log is partitioned into sharing scopes, ops are authorised against the author's membership at receipt and refused ones are reported per op. Sharing scopes, API
- Task assignment (
assignee_id), the Assigned to me view and in-app notifications (GET /api/v1/notifications). Assignment - Threaded Markdown comments (
commententity) with@mentions, and an activity log derived from the op log, per task and per project. Comments - Activity feed across shared projects with unread counts (
project_prefsentity, personal scope only;GET /api/v1/activity). Activity feed - Invitations: single-use, expiring links in Settings → Members. Invitations
- User administration: admin role, deactivation, deletion, password reset links, owner transfer, own password change and
opentodo reset-owner-passwordon the host. User administration - Data import of the app's own export, CSV and JSON exports of other task managers, with preview, duplicate detection and undo (
OPENTODO_IMPORT_MAX_MB, default 20). Import - Scheduled backups to a folder, S3-compatible storage or WebDAV with retention, optional
OTBK1encryption,OPENTODO_BACKUP_*presets andopentodo restore; sync responses carry a loggenerationso devices re-send newer edits after a restore. Backups - Signed outbound webhooks with a durable queue, retries, delivery log and SSRF protection (
OPENTODO_WEBHOOKS_ALLOW_PRIVATEpreset). Webhooks - CalDAV two-way sync under
/dav/with per-device app passwords (otap_…), off until the owner enables it (OPENTODO_CALDAVpreset); new task fieldical_extrakeeps unmapped iCalendar properties. CalDAV - Read-only MCP endpoint at
/mcpfor AI assistants (five tools,opentodo://resources), off until the owner enables it (OPENTODO_MCPpreset). MCP - Passkeys (WebAuthn) when
OPENTODO_BASE_URLis https; verification ininternal/webauthn. - Two-factor authentication (TOTP) with recovery codes, remembered browsers, an owner policy (
none,owner,all), step-up re-authentication for sensitive routes andopentodo mfa-reseton the host. - Single sign-on with one OpenID Connect provider (PKCE, JWKS verification with the standard library), optional
OPENTODO_OIDC_*overrides, account linking and an SSO-only mode with owner break-glass. SSO - Audit log of security events with retention, NDJSON export and opt-in client addresses. What is logged, API
- Optional end-to-end encryption of task content per account (Web Crypto, AES-256-GCM, passphrase plus recovery key); routes under
/api/v1/encryption/. E2EE - Demo mode for a public demo instance:
OPENTODO_DEMO=truecreates a hidden owner and a shared demo account shown on the sign-in screen ("Try the demo"), seeds realistic data through the op log, wipes and reseeds it every night at 03:00 UTC (devices recover through purge markers, sessions stay), shows a banner, and refuses sign-up, account changes, integrations, import and encryption with 403demo_restricted; no outbound requests. Demo instance
Changed
- Deleting a project only tombstones the project; its tasks keep their
project_idand come back when the project is restored from the Trash. - API tokens have a scope:
readtokens are refused on every write route and on/api/v1/sync(403 insufficient_scope); existing tokens stayfull. /api/v1/syncresponses gained additive fields:rejected,memberships,purged,trashRetentionDaysandgeneration; requests may carrytzOffset. Served ops carryactor. Older clients ignore them.- REST collections and the export include projects shared with you (with read-only
roleandowner_id), and the export gainedlabels,views,reminders,sections,commentsand theencryptionkey record. The export format version stays 1. otembeds Go's time zone database so zoned due times resolve on hosts without one (Windows, distroless).- Server dependencies:
github.com/fxamacker/cbor/v2(passkeys). Web dependencies:qrcode-generator(two-factor QR codes),yjsand theprosemirror-*packages (rich-text notes, lazy-loaded). Everything else uses the standard library and existing packages.
Fixed
- Imports no longer drop sections, recurrence and reminders. An own export now round-trips its sections, reminders and saved views with their ids (so section placement, recurring series and reminder links stay intact) and every task field, with recurrence stored canonical; reminders whose time has passed undelivered are imported dismissed so no stale notifications go out. Comments are reported in the preview but not imported (a comment keeps its author), and assignees other than you are dropped with a warning. CSV and the JSON formats of popular commercial task apps map recurrence strings (rule grammar, RRULEs, common spellings, structured objects; anything else is a preview warning), reminders (absolute and minutes before the due time) and sections. The
sectionsimport option gainedsection, now the default (labelkeeps the old behaviour); the preview counts new sections, recurring tasks and reminders; undo also removes imported sections, reminders and views. Import - Reminders on tasks in a project in the Trash (or under a sub-project whose ancestor is in the Trash) no longer fire, neither as web push nor in the app. Restoring the project brings them back: a reminder whose time passed while the project was trashed is delivered once, late, like after server downtime. Reminders
/api/v1/syncpull pages, including the history replay for a newly joined shared project, are capped at about 4 MB of op values (always at least one op), so a batch of large rich-text notes no longer produces one huge response;hasMoreasks the device to continue. Sync- Purging a task, a project (with its tasks) or emptying the Trash by retention now removes the task's CalDAV resource name in the same transaction; migration
0024removes rows earlier purges left behind. CalDAV - A large import sent one webhook delivery per task. Imports and their undo now produce one
import.completedorimport.undonedelivery per subscribed webhook with counts and project ids; ops written by an import no longer emittask.*/project.*events, so a webhook that wants imports subscribes to the new events. Ordinary writes keep their per-event deliveries. Webhooks - Opening the app at a different host than
OPENTODO_BASE_URLmade passkeys fail with the browser's "origin is not authorized for the provided RP ID"; the app now explains which two hosts differ and what to change, and Settings → Security shows the host passkeys are tied to (alsopasskeyDomaininGET /api/v1/auth/status). Indocker-compose.ymlanOPENTODO_BASE_URLyou set yourself now wins over Coolify's generatedSERVICE_URL_OPENTODO. - Dropdown menus (view, section, project and label menus) could open mostly off screen on phones when the header wrapped; they now shift back inside the screen when opened.
- The keyboard-shortcut help could not be scrolled or closed on phones (the panel grew past the screen on iOS Safari). It is now a full-screen sheet on small screens with a pinned close button, the overlay scrolls instead of the panel, and every other dialog overlay scrolls when its content is taller than the screen.
- Clients skip ops for entities they do not know instead of failing the whole batch, so an installed PWA that has not refreshed yet keeps syncing when the server gains a new entity.
- A reference to a deleted, foreign or not-yet-received section or label is displayed as "No section" or hidden without writing a repair op, so arrival order can never overwrite a newer edit.
Security
- Sign-up is closed after the owner account; new people join by single-use invitation links or, if allowed, single sign-on.
- Sensitive routes (new API tokens, app passwords, invitations, user administration, two-factor and SSO settings, encryption key changes) require a sign-in or re-authentication within ten minutes on cookie sessions (
403 reauth_required). - Rate limits on sign-in, registration, passkeys, the second sign-in step, re-authentication, invitation and reset previews, single sign-on, comment creation, admin actions and failed
/mcpand CalDAV authentications. - Webhook delivery refuses loopback, private, link-local, metadata and other internal targets unless the owner allows private networks, and never follows redirects.
- CalDAV accepts only app passwords;
/api/v1refuses them./mcpaccepts only bearer tokens and checksOrigin. - Credentials, secrets and audit details are kept out of logs and the audit log by an allow-list; secrets for backups and single sign-on live in
<data>/secrets/(mode 0600), outside the database and backups. - See SECURITY.md for the full model.
Known limitations and verification follow-ups
- CalDAV with real clients: Apple Reminders, Thunderbird and DAVx⁵ (Android tasks apps).
- Single sign-on against a real OpenID Connect provider such as Authentik or Keycloak (tested against an in-process provider).
- Email to task against real mail providers' IMAP servers (tested against an in-process IMAP server).
- MCP with real AI assistant clients.
- Voice capture with a real microphone in Chrome and Safari.
- Web push on real devices and push services.
- Passkeys on real devices and authenticators.
- Share target behaviour in Firefox (and other platforms listed in docs/capture.md).
- Touch drag and drop (board, manual ordering, calendar) on real touch devices.
Your to-do list, on your own server.
Try the demo in your browser. When you like it, install OpenTodo on your server and import your tasks.