OpenTodo 0.3.0

Released on

OpenTodo 0.3.0 is about bringing your own AI, if you want one. There is a chat assistant inside the app that answers questions about your tasks and proposes changes you review and undo in one step, with Anthropic, OpenAI, Gemini or a model on your own network. An assistant you already use can now make changes over MCP, and the ot command is ready to be driven by coding agents. All of it is off until the owner turns it on; nothing is sent anywhere before that. Use AI with OpenTodo compares the three options. Settings is also split into sections with their own pages, and a project can be shared with a single invite link.

Upgrade notes

  • Migrations 0026 to 0029 (agent tokens and switches; assistant consent and usage; members' own assistant provider; project invite links) are applied automatically at startup. Back up /data before upgrading: there are no down-migrations. No new environment variables are required; OPENTODO_MCP_WRITES is optional.
  • Image tags. The release publishes ghcr.io/pixari/opentodo:0.3.0 and :0.3 for linux/amd64 and linux/arm64 (:latest follows main). The Coolify template now follows :0.3; if you use :0.2, change the tag in Edit Compose file and click Deploy.
  • ot scripts: destructive commands now need --yes off a terminal, and refusals exit 5 instead of 1 (see Breaking below).
  • Binary size. The server binary grows from about 17 MB to about 23 MB, mostly the official Anthropic SDK used by the assistant.

Breaking

  • ot: destructive commands need --yes without a terminal. ot rm, ot project rm, ot token revoke and ot auth logout used to proceed silently when stdout was not a terminal or --json was given. They now refuse with exit code 2 (confirmation_required) before sending anything. Migration: add --yes to scripts and cron jobs that delete on purpose, e.g. ot rm "$ID" --yes. On a terminal without --json they still ask. (CLI)
  • ot: new exit code 5, "not permitted". Refusals for scope, role, a switch, encryption or read-only mode (insufficient_scope, forbidden, not_permitted, writes_disabled_by_owner, writes_disabled_by_user, encryption_enabled, read_only) exit 5 instead of 1, so scripts and agents can stop instead of retrying. Migration: scripts that test for exactly 1 should also accept 5; scripts that test for non-zero are unaffected. The op log and the REST API are unchanged.

Added

  • Invite to a project with a link. In a project's Share dialog, the owner and admins can create a single-use invite link with a role (viewer or editor; admin for the owner), an optional email binding and an expiry from 1 hour to 30 days (default 7 days). The link is shown once; pending links are listed with their role, email, expiry and creator and can be revoked. Opening /invite/p/<token> shows who invited you to which project: signed in, you select Join project; otherwise you sign in and join, or create an account that joins in the same step. A link creates accounts only when its creator is the server owner or an admin, when open sign-up is on, or when the owner switches on Members can invite new people with project links in Settings → Administration (off by default); otherwise the page asks the person to get an account first. Links stop working when revoked, used or expired, when the project is deleted, or when their creator is no longer the project's owner or an admin. Links are server-side data, never synced; refused on the demo instance. (Sharing, API)
  • New routes: GET/POST /api/v1/projects/{id}/invites, DELETE /api/v1/projects/{id}/invites/{inviteId}, GET /api/v1/project-invites/{token} (public preview), POST /api/v1/project-invites/{token}/accept, GET/PUT /api/v1/settings/project-invites; POST /api/v1/auth/register accepts projectInvite. New error code project_invite_not_allowed; audit events project_invite.created, project_invite.revoked, project_invite.accepted. Migration 0029_project_invites.sql is applied automatically.
  • Your own assistant provider. When the owner lets members bring their own key, each person can choose Anthropic, OpenAI or Google Gemini, a model and their own key in Settings → AI → Assistant → Which assistant you use, with Test connection. Members are never offered the compatible endpoint, so they cannot make the server connect to an address of their choosing. The disclosure names the provider actually used ("from this server to … with your own API key") and is asked again when it changes. Turning member keys off makes everyone use the owner's provider again; choices are kept for later. Keys stay in <data>/secrets/assistant.json, never returned, logged, exported or backed up. (AI assistant)
  • Own keys outside the allowance. Answers on a member's own key are recorded and shown as "your key", but neither counted nor stopped by the monthly token allowance unless the owner turns on the new Count members' own keys toward the monthly limit (cap_member_keys). One answer per person and 8 per server still apply. The owner sees how many people use their own provider and key.
  • API (additive): own_provider, own_model and new status fields on GET/PUT /api/v1/me/assistant; POST /api/v1/me/assistant/test (signed-in sessions, 5 per 10 minutes); cap_member_keys and own_key_members on /api/v1/settings/assistant; counted_tokens and own_key on the usage route and own_key on the turn's usage event. (API)
  • AI assistant in the app. A chat panel (Shift+I or Assistant in the header; a side sheet on wide screens, full screen on phones) that answers questions about your tasks and proposes changes: "move everything overdue to Friday", "break this task into steps". Each proposal is a reviewable list with every change in words; you apply the checked part (at most 50 changes) and undo it in one step. Applied changes are ordinary edits from your device: offline-capable, synced, labelled Assistant in history. Providers, each with the owner's or a member's own key: Anthropic through the official Anthropic Go SDK (default claude-opus-5-5; claude-sonnet-5-5 and claude-haiku-5-5 offered), OpenAI, Google Gemini and any OpenAI-compatible endpoint, including Ollama or LM Studio on your network. Off by default: the owner enables it in Settings → Assistant (provider, models, keys, private endpoints, monthly token allowance, member keys, Test connection); each person accepts a disclosure naming the provider and the fields sent before their first message, and again when the provider changes. Keys live only in <data>/secrets/assistant.json, never returned, logged, exported or backed up. The assistant cannot change sharing, members, settings or tokens, make outbound requests or delete permanently; task text is marked as untrusted. Conversations stay on the device and are deleted at sign-out. Not available for end-to-end encrypted accounts or on the demo instance. Optional per-device auto-apply of up to 3 small, reversible changes, off by default. (AI assistant, API, security)
  • New routes: GET/PUT /api/v1/settings/assistant, POST /api/v1/settings/assistant/test (owner), GET/PUT /api/v1/me/assistant, GET /api/v1/me/assistant/usage, and POST /api/v1/assistant/turn (signed-in sessions only; streams server-sent events). /api/v1/sync accepts the op device prefix assistant:; history entries from it carry device_label: "Assistant".
  • Dependency: github.com/anthropics/anthropic-sdk-go (the server binary grows by about 5 MB; see the change's design notes).
  • ot for agents and automation (CLI): --json output is documented with JSON Schemas and output_version 1 (ot version --json); ot schema (or ot help --json) prints every command, flag, exit code, error code and output schema, offline; --dry-run on every write command resolves references and prints the requests it would send without writing; --idempotency-key on ot add and ot project add makes retries return the same entity; ot done/ot reopen on a task already in that state report unchanged and send nothing, and ot edit with unchanged values sends nothing; --read-only (or the optional OPENTODO_READ_ONLY=1) refuses every write before any request; ot refuses writes locally for read tokens and outside the allowed routes for agent tokens; ot auth status shows the token's scope and whether assistant writes are allowed. With --json, errors carry an additive exit field and stderr gets one ot: line; done/reopen/rm add ref and status to each task.
  • Agent skill file. ot skill prints SKILL.md, which teaches an agent to use ot safely (agent token, --json, dry runs, idempotency keys, exit codes, task text as untrusted data). Each release ships it as opentodo-agent-skill.zip.
  • Idempotency-Key on POST /api/v1/{collection}: the entity and op ids derive from your account, the collection and the key, so a retried create returns 200 with the stored entity and Idempotent-Replayed: true; the same key with other content answers 409 idempotency_conflict. Keys never cross accounts and need no storage. (API)
  • agent tokens on REST content routes: creating tasks, projects, labels and comments, editing tasks, renaming or recolouring projects and trashing the token's own tasks, under the same two switches, limits, agent.write audit, history label and encryption rules as the MCP write tools (they run through the same tool registry). Every other REST write still refuses them. (API)
  • GET /api/v1/me gains an additive auth object: method, scope, token_id, token_name and, for agent tokens, writes_allowed/writes_denied. Never the secret. (API)
  • AI assistants can make changes, when you allow it. The MCP endpoint gains ten write tools: create_tasks, update_tasks, complete_tasks, reopen_tasks, move_tasks, schedule_tasks, create_project, create_label, add_comment and trash_tasks. They appear only when the owner turns on AI assistants may make changes, you turn on Allow AI assistants to make changes to my tasks (both off by default, in Settings → AI assistants), and the assistant uses a new agent token. Full-access and read-only tokens keep read-only tools. Every change goes through the op log, syncs and converges like any edit, shows in history as "AI assistant (token name)", is recorded in the audit log as agent.write (no content), and can be undone. Nothing is ever deleted permanently; trash_tasks only accepts tasks the same token created; assistant comments notify no one. Limits: 50 items per call, 30 write calls per minute and 1,000 changed items per day per token. For end-to-end encrypted accounts, tools that would write readable text are not offered (encryption_enabled); scheduling, completing and moving still work. (AI assistants)
  • agent token scope: refused on every REST write like read, except the agent content routes above (it cannot create tokens, change settings or sync raw operations). Create one in Settings → AI assistants, Settings → API tokens, or with ot token create <name> --scope agent; ot token list shows each token's scope.
  • GET/PUT /api/v1/me/agent-access (your own switch; sessions and full tokens only) and writes_enabled on /api/v1/settings/mcp. The optional OPENTODO_MCP_WRITES=true presets the owner's switch on first start.
  • New internal package internal/agenttools: one registry of agent tools (definitions, validation, planning, limits, permission gate), to be shared with the planned in-app assistant.
  • Coolify one-click template, ready to submit upstream. deploy/coolify/opentodo.yaml now follows Coolify's current template conventions: SERVICE_URL_OPENTODO_8080, the 0.2 image line, a /data volume and a /opentodo healthcheck health check. It also lists the useful optional variables with their defaults (including OPENTODO_TRUSTED_PROXIES and email to task), because Coolify passes a variable from its UI only when the compose file references it. An explicit OPENTODO_BASE_URL wins over Coolify's generated URL. deploy/coolify/upstream/ mirrors Coolify's repository paths and holds a draft docs page, the pull request text and step-by-step submission notes. Until the template is listed in Coolify, paste it as a Docker Compose resource. (Coolify)
  • scripts/coolify-template-check.mjs (in make check and CI) fails when the template breaks those conventions, names an OPENTODO_* variable the server does not read, lags behind the latest release, or differs from its upstream copy. make coolify-smoke starts the image the way Coolify does and checks the health check, the owner setup, closed sign-up, the https base URL and data surviving a redeploy.

Changed

  • An invalid OPENTODO_TRUSTED_PROXIES entry no longer stops the server. A typo such as cloudflar used to fail startup, so the health check failed and Coolify rolled the deploy back with the cause only in the container log. Now the server starts, ignores the invalid entries (keeping the valid ones; with none valid it behaves as unset, so trust is never widened), logs one error line naming them with a "did you mean cloudflare?" hint, and shows the owner a Server configuration warning at the top of Settings and Settings → Administration. Entries may also be separated by spaces. GET /api/v1/me gains an additive config_warnings array for the owner's session only. Every other invalid variable still stops startup. (Behind a reverse proxy)
  • Settings has sections. Instead of one long page, /settings lists the sections with a line on what each holds, and each opens on its own page: Account, General, Notifications, Data, Capture, Integrations, AI and, for the owner and admins, Administration (/settings/account, /settings/general, …). Wide screens keep the section list beside the content; phones get a back link. Every setting works as before, Help links open the same guides, and /settings/import is unchanged. The user guide now names the section, for example Settings → AI → AI assistants. The single sign-on link flow returns to /settings/account. (Settings)
  • MCP read results mark text people wrote: titles, descriptions, names and comment bodies now sit inside an untrusted_text object, every result carries a _notice telling the assistant to treat that text as data, tasks of shared projects carry shared: true and comments author_is_you. Label ids are listed as label_ids next to the names. The tool definitions themselves are unchanged. (AI assistants)
  • /api/v1/sync refuses operations from devices whose id starts with agent: or caldav: (400 invalid_op): those ids are reserved for the server's own writers.
  • API requests with a token scope this server does not know are refused on every write route (previously only read was).
  • Migration 0026 adds two side tables (api_token_agent, user_agent_access) and is applied automatically at startup. Rolling back to an older version is safe: agent tokens then act as read-only tokens.
  • Images built from source by Coolify report dev+<commit> instead of dev when the application's Source commit availability is Available during build (the Dockerfile reads SOURCE_COMMIT).
  • Coolify guide: which resource types roll and which recreate, why plain Restart and API deploys keep a cached image, first-visit owner setup, domains and passkeys, Cloudflare (proxy and Tunnel, Browser Cache TTL), and Coolify volume backups.

Fixed

  • A reminder that became due while a dialog, menu or popover was open took the keyboard focus out of it. It is still announced, but focus now stays where you are.
  • Phones could keep running the previous release after an update when a CDN such as Cloudflare sat in front: it cached the service worker script for hours. Service worker scripts are now sent with Cache-Control: no-store (and CDN-Cache-Control: no-store).

Downloads and checksums for v0.3.0

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.

OpenTodo is free and open source, with no paid tier. If it helps you, a star on GitHub helps other people find it, and a coffee helps keep it going.

Buy me a coffee