Shared tasks for the people you share life with.
Ditero is a free, self-hosted, local-first task app for families, friends, clubs, and small teams. Share a shopping list, divide the chores, plan a project, or keep up with daily habits. Already-open browser tabs work offline and sync when you're connected again; opening or reloading the app needs a connection.
Your data lives on your server. Reminders, attachments, calendars, and sharing are included, with no subscriptions or paid feature unlocks.
Website | Quick start | Features | Documentation | Roadmap | Changelog | Brand assets
Beta. The latest published prerelease is
v0.1.0-beta.5, a0.xprerelease with breaking changes possible beforev1.0.0. It is not stable. The changelog describes it and earlier releases. The native desktop archive-import entry remains unavailable pending transfer and recovery qualification. Native device/platform qualification, general deep links and automatic updates remain incomplete in #346. The release pipeline packages server containers, Helm/Kustomize/Compose files, desktop installers and signed Android downloads. Windows builds remain unsigned; macOS builds use ad-hoc signatures without notarization. See release instructions and published downloads.
The capabilities below describe develop; unreleased additions are available
from source or nightly images, and published prereleases contain a subset.
Keep personal lists alongside shared workspaces. Assign tasks, leave comments, and invite people with the access they need. Shopping lists, recurring chores, and habit streaks cover the everyday work that falls between a calendar and a chat.
The interface supports six languages, including Arabic and RTL layout. Choose light, dark, or system mode, then choose an accent: teal by default, or blue, clay, violet, berry, or ochre. Reading-size presets and high contrast are independent of those choices. Palettes and accent preferences sync with your account; reading size and high contrast stay on each device. The last workspace and list also stay on each device, separately for each account. If access ends or a list is deleted, navigation falls back to an available workspace or its home view. Saved list templates use the name you enter, or the template name when you leave it blank.
- Unified typed lists: tasks, shopping lists, checklists, and projects
- Subtasks, labels, priorities, due dates, folders, and drag-to-reorder
- Recurring tasks and habits/chores with flexible recurrence (RFC 5545 RRULE) and streaks
- Reminders with escalation and acknowledgement, delivered to ntfy, Telegram, Discord, Slack, or email
- Display names at sign-up and in Settings > Account, shared in member and assignee displays. Names, including managed-account names, use a trimmed 1-80-character limit without control characters.
- Multi-workspace sharing with Owner / Admin / Member / Viewer roles (Viewer task controls are read-only and explain the restriction). Email-bound invites opened with another account offer sign-out so you can use the invited account.
- Leave shared workspaces from your own Members row; the last owner must transfer ownership first
- Invitation links, managed accounts, comments, and recorded task completion history
- Email/password, passkeys, TOTP, recovery codes, and optional Google sign-in
- English, German, Spanish, French, Romanian, and Arabic, including RTL layout
- Light, dark, and system modes, six accent themes, reading-size presets, and high contrast
- Named light/dark palettes, runtime color editing and validated JSON sharing; custom palettes sync with your account
- Focus timers resume after reload on the same device, including paused and task-bound intervals
- Saved views, dashboards, calendar/board/table layouts, and keyboard shortcuts
- Encrypted attachments with filesystem or S3-compatible server storage
- Authenticated encrypted attachment archives and reviewed, resumable attachment migration with explicit parent mapping and durable replay
- JSON export and reviewed, resumable native import, including version 2 history and attribution, plus Ditero and generic CSV, Todoist project CSV, TickTick backup CSV, Microsoft To Do via classic Outlook task CSV, and Trello board JSON, with located preview notices for skipped rows, bound Outlook date order and reuse of same-named workspace labels; see data portability for exclusions
- Browser installation plus Android and desktop development apps; see native app status
- Membership-scoped public API with expiring personal access tokens, OpenAPI, list, task and folder CRUD, observed relationships and placement, task comments, personal-access-token iCalendar export, list calendar subscription feeds and list-bound inbound webhooks; see API access
- CLI and local MCP tools for discovery, task planning, and task, list, folder, comment, relationship, placement and webhook workflows, plus an interactive terminal UI for task creation, completion, scalar updates, deletion and reordering, with read-only comments; released as JavaScript bundles that run on an installed Bun 1.3.14 or newer (install)
iOS and direct Microsoft PST import remain pending. Provider imports require review of exclusions and exact snapshot identity; they are not account synchronization. Wider attachment migration runtime, terminal, native and deployment qualification remain pending. Quick add has voice dictation with review before use. It runs on-device where the browser supports it; browsers that only offer the standard Web Speech API may send audio to the browser vendor's speech service, so a notice must be accepted first. Dictation stops when the tab is hidden. It is unavailable in the native apps, and real-device qualification is pending. Calendar subscriptions can be created and revoked in web settings; a new subscription URL is shown only once. The roadmap distinguishes delivered capabilities from remaining work.
Focus timers are stored locally for each account and shared by its tabs on the same browser or native server profile. Paused time is excluded. Stop discards the unfinished interval in every tab. Clearing site data removes local timer state. Timers do not follow you to another device.
Online starts use exclusive server-observed spans per account. Starting another interval closes the previous span; credit requires the full duration plus pauses within that span, without a per-interval time allowance. A forward clock change cannot multiply credit across overlapping intervals. Early completions retry; superseded intervals without enough time are rejected.
After reload, a persisted acknowledgement alone is insufficient: the current server start must sync before it can establish credit. An observer without proof leaves the shared interval unresolved so another observing tab or a delayed sync can complete it. After ten seconds a notice explains the missing confirmation; the timer can still reconcile during the server's 14-day acceptance window. Offline intervals use the completing page's monotonic elapsed time and progress saved when paused. Pending completions retain whether they used that proof or a server start; a missing server start never silently falls back to offline proof. Focus totals are personal records, not tamper-proof time tracking: legacy and offline clients supply their own timing evidence.
| Layer | Choice |
|---|---|
| Sync engine | Zero (local-first, query-based sync) |
| Backend | Elysia on Bun |
| Frontend | React 19 + shadcn/ui + Radix + Tailwind v4 |
| Auth | Better Auth (email + OAuth, JWT for Zero) |
| Database | PostgreSQL 18 (wal_level=logical) via Drizzle ORM |
| Native | Capacitor (mobile) + Tauri 2 (desktop) + PWA |
| i18n | Paraglide JS |
The deploy/docker stack runs the app (web UI + API served
same-origin on one port), PostgreSQL, and the Zero sync cache.
Clients collaborate through this shared server. PostgreSQL 15 or newer is required
(the bundled image uses PostgreSQL 18). PostgreSQL must provide logical
replication and direct connections; PGlite and other embedded databases are not
supported substitutes. See database roles.
Published images are on GHCR, so no checkout is needed to run it — but the Compose file is in this repo, so either clone it or download that one file.
# From the repo root, create this private file once for a new installation.
(
set -euC # Refuse to overwrite an existing file.
umask 077
cat > deploy/docker/.env <<EOF
POSTGRES_PASSWORD=$(openssl rand -hex 24)
DITERO_MIGRATION_DB_PASSWORD=$(openssl rand -hex 24)
DITERO_RUNTIME_DB_PASSWORD=$(openssl rand -hex 24)
BETTER_AUTH_SECRET=$(openssl rand -hex 32)
DITERO_ENCRYPTION_KEY=$(openssl rand -base64 32)
ZERO_ADMIN_PASSWORD=$(openssl rand -hex 32)
EOF
docker compose --env-file deploy/docker/.env \
-f deploy/docker/docker-compose.yml --profile bundled up
)Then open http://localhost:3000 and sign up. The first account becomes the owner; later ones need an invitation.
Reuse and back up that environment file for restarts and upgrades. Do not regenerate
database passwords or encryption keys for an existing volume. The file is gitignored;
mounted secrets with _FILE variables are also supported.
Compose file secrets are bind mounts: their files and parent directories must be
readable/traversable by each container's actual UID/GID, including PostgreSQL's
postgres user and the app's UID 1000. A host-owned mode 0600 file usually fails
this requirement; use narrowly scoped ownership, groups or ACLs. Keep secrets
private and follow initialization recovery.
Bundled raw password variables support URL delimiters such as @:/%#?; the app
and Zero entrypoints percent-encode them when building DSNs. Explicit DSNs must
already percent-encode credentials. Do not print DSNs in diagnostic reports.
That pulls the published 0.1.0-beta.5 app and matching Zero images by default.
Add --build to build from this checkout instead, or set
DITERO_IMAGE_TAG=nightly to use development images. Commit-specific nightly tags use different suffix ordering for app and
Zero, so pin those two service images separately in a Compose override; see image tags.
To reach it from anything other than the machine it runs on, set
BETTER_AUTH_URL and PUBLIC_ZERO_URL to addresses that machine's browsers can
resolve.
Serve the application at its own origin root, such as https://tasks.example.com/
or a dedicated port. An app path prefix such as /ditero/ is unsupported: assets
and API routes use root-relative paths. Zero alone can use a proxy path prefix
when the proxy strips that prefix, forwards WebSocket upgrades, and
PUBLIC_ZERO_URL includes the externally visible prefix.
BETTER_AUTH_URL names exactly one origin, and auth routes reject every other
one with 403. To serve the same instance at more than one address — a LAN
name and localhost, say — list the extras in TRUSTED_ORIGINS:
BETTER_AUTH_URL=http://ditero.example.lan:3000 \
TRUSTED_ORIGINS=http://localhost:3000 \
docker compose --env-file deploy/docker/.env \
-f deploy/docker/docker-compose.yml --profile bundled upThe same variable is the CORS allowlist outside production, where it also grants those origins cross-origin API access. Production disables CORS outright, so in the Compose stack it only widens the auth trusted-origin list.
All configuration is environment-driven. The common variables:
| Variable | Default | Purpose |
|---|---|---|
BETTER_AUTH_SECRET |
(required) | At least 32 characters in production. Generate with openssl rand -hex 32. Changing it requires the key rotation procedure; replacing it directly breaks sign-in. |
ZERO_ADMIN_PASSWORD |
(required) | Admin password zero-cache requires in production. Generate with openssl rand -hex 32. |
DITERO_ENCRYPTION_KEY |
(required) | 32-byte Base64 key for stored replayable secrets. |
DITERO_MIGRATION_DB_PASSWORD |
(required, bundled) | Password for the schema-owner role. |
DITERO_RUNTIME_DB_PASSWORD |
(required, bundled) | Password for the non-owner application role. |
BETTER_AUTH_URL |
http://localhost:3000 |
Public base URL the app is served from. |
TRUSTED_ORIGINS |
empty | Comma-separated extra origins auth routes accept, on top of BETTER_AUTH_URL. Also the CORS allowlist outside production. |
DITERO_DATABASE_URL |
bundled Postgres | Non-owner application Postgres DSN. |
DITERO_MIGRATION_DATABASE_URL |
bundled Postgres | Schema-owner migration DSN. |
DITERO_ZERO_DATABASE_URL |
bundled Postgres | Direct, replication-capable Zero DSN. |
ZERO_APP_PUBLICATIONS |
unset | Comma-separated precreated Zero publications for a non-superuser replication login; see database roles. Empty leaves Zero's default unchanged. |
ZERO_AUTO_RESET |
unset | Set true to rebuild the Zero cache when replication/schema changes require a reset. Empty leaves Zero's default unchanged. |
POSTGRES_PASSWORD |
(required, bundled) | Password for bundled PostgreSQL and Zero's bundled connection. |
DITERO_TRUSTED_PROXIES |
empty | Comma-separated CIDRs allowed to supply forwarding headers. |
PUBLIC_ZERO_URL |
http://localhost:4848 |
Address browsers dial zero-cache on. Served to the web client at runtime and used for the CSP. |
DITERO_ZERO_SHARD_SCHEMA |
zero_0 |
Schema zero-cache keeps sync bookkeeping in. Both app roles need access to it; see database roles. |
DITERO_IMAGE_TAG |
nightly |
Image tag the Compose stack runs. The zero-cache image is that tag plus -zero. |
DITERO_REGISTRATION_MODE |
bootstrap |
open, bootstrap (first account plus eligible invitations), or closed (eligible invitations only). |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
empty | Optional Google sign-in from your own OAuth client; both are required. See sign-in providers. |
DITERO_OIDC_PROVIDERS, MICROSOFT_*, GITHUB_*, DISCORD_*, FACEBOOK_*, APPLE_* |
empty | Optional generic OpenID Connect providers (DITERO_OIDC_<ID>_*) and Microsoft, GitHub, Discord, Apple and Facebook sign-in; every secret also accepts _FILE. See sign-in providers. |
DITERO_SAML_IDP_METADATA_URL, DITERO_SAML_IDP_METADATA, DITERO_SAML_IDP_METADATA_FILE |
empty | Optional SAML single sign-on; set exactly one. See SAML single sign-on. |
Note: the web client fetches
PUBLIC_ZERO_URLfrom/api/configat startup, so one built image serves any hostname. Set it to the address browsers reach zero-cache on, not an internal service name — and expose that address, or sync cannot connect.
The bundled profile runs upstream-db (Postgres 18 with wal_level=logical).
To use your own PostgreSQL 15+ instead, provide separate runtime, migration-owner, and
Zero DSNs and omit that profile:
DITERO_DATABASE_URL=postgres://runtime:pass@db.example.com:5432/ditero \
DITERO_MIGRATION_DATABASE_URL=postgres://owner:pass@db.example.com:5432/ditero \
DITERO_ZERO_DATABASE_URL=postgres://zero:pass@db.example.com:5432/ditero \
docker compose --env-file deploy/docker/.env \
-f deploy/docker/docker-compose.yml up --build app zero-cacheReuse the private secrets file created above; replace the example DSNs with your database credentials. The runtime role must not own tables or bypass RLS. The Zero DSN must be direct, non-pooled, and able to create replication slots. See security architecture, database roles, and the backup/restore runbook.
Reminders for due tasks and habits, plus assignment, mention, and overdue notices, delivered through a durable outbox with retries, escalation, quiet hours, and one-tap acknowledgement. ntfy, Telegram, Discord, Slack, and email all deliver today.
One-tap acknowledgement from the message itself works on ntfy and Telegram, and on Discord and Slack in app mode. A pasted incoming webhook cannot carry an interactive button on either platform, so Discord and Slack each offer a webhook mode (send-only) and an app mode; app mode requires a public base URL for the inbound listener and is refused at save time without one, rather than saved and left quietly non-interactive. Telegram defaults to polling, which is outbound only and needs no public URL, certificate, or forwarded port.
Delivery is at-least-once, never exactly-once — you can receive a duplicate — and there are bounded conditions under which a notification is dropped entirely. Reminders are not medical-grade. Read docs/notifications.md before relying on this for medication, and security architecture before pointing a channel at a host on your own network.
All scheduler and worker knobs, with their defaults and the boot-validated ordering constraints between them, are documented in .env.example.
Encrypted attachments are disabled by default. To opt in, add
DITERO_E2E_ENABLED=true to the private Compose environment file and recreate the
app container with the same Compose command. While disabled, /api/e2e/* and
/api/attachments/* return 404. Browser encryption requires HTTPS or a
browser-secure loopback origin with Web Crypto available, plus account key
enrollment and a workspace key grant. Plain HTTP on a LAN is insufficient.
Files attached to tasks, comments, and lists are encrypted on the client before upload. The server stores and proxies only ciphertext, including encrypted filenames, declared media types, and thumbnails. It can still see the parent record, uploader, byte counts, lifecycle state, and storage location because authorization, quota enforcement, and garbage collection depend on them. Browsers without writable private-file staging support uploads up to 8 MiB of combined encrypted file and thumbnail data; server limits and workspace quotas still apply. Attachment tiles show the original byte count when it can be recovered exactly from the encrypted framing. Otherwise, "Stored size" identifies the encrypted byte count, including framing overhead.
The Compose default stores ciphertext on a persistent filesystem volume. S3-compatible storage is also supported without exposing the bucket to browsers. Uploads require a live connection, and the server cannot scan encrypted files for malware. Read the security boundary, storage and backup runbook, and key-loss runbook before enabling attachments for data that has no other copy. All attachment configuration is documented in .env.example.
Production browser builds include a PWA manifest and a bounded offline public app shell. The service worker caches static public files, with no API, auth, Zero, or private user responses. Opening or reloading the browser app offline shows a connection-required page instead of opening cached workspaces. It retries automatically when the connection returns, with a Retry button for manual attempts. Already-open tabs keep working offline and sync queued edits after reconnecting. Private synced records remain in Zero's separate local storage; startup still requires live session verification. Updates show a notice and reload only on user choice, after durable local sync retirement. HTTPS and browser-secure loopback origins are supported. Browser qualification covered installability, offline shell recovery, and manual update; it did not perform an OS installation or test a signed-in queued Zero edit during update.
The installer pipeline builds Linux x64 DEB/RPM/AppImage, unsigned Windows x64/ARM64 NSIS, and ad-hoc macOS Apple Silicon/Intel DMGs. These are the targets for the next release; the current published release keeps its existing downloads. PR development installers expire after seven days and include checksums. Installer smoke checks cover payloads and bounded startup, with qualification limits in the desktop guide. Linux ARM64 installers are not built.
The Android and desktop
development apps support native browser-consent sign-in, scoped local storage,
and encrypted attachment upload, download/save, delete, and in-app image preview.
Encrypted files require DITERO_E2E_ENABLED on the server and account key enrollment.
Native credentials stay outside JavaScript; file transfers use named native HTTPS
operations and system save pickers, with complete integrity verification before
plaintext save writes. Bounded Android and Linux desktop file journeys have passed.
Android supports encrypted UnifiedPush notices and task navigation; a bounded emulator
check covered reception while stopped in deep Doze with an exempt ntfy distributor.
Linux desktop notices arrive while the app is open or minimized and can open the task.
These notices contain generic text; opening them does not complete or acknowledge a task.
The optional Google push relay requires separate operator
configuration and remains unqualified for real delivery. Physical-device coverage,
Windows/macOS notifications, general deep links, updates, and trusted desktop signing
remain unfinished. See each app's guide for the precise qualification limits.
Nightly app and Zero images publish to GHCR for amd64 and arm64. The release workflows
provide GHCR and Docker Hub images and an Alpine app default when a release is cut.
The Debian app variant is held out of releases until Debian 13 fixes zlib
CVE-2026-76844 (#1267). :latest and :stable are release channels, not current nightly
tags. Prereleases use explicit version tags and never move latest or stable.
A Helm chart packages app and Zero with an external
PostgreSQL database. Kustomize packages also support
externally managed PostgreSQL, with an optional CloudNativePG overlay. Live cluster
install, upgrade and restore qualification remains in progress. See
RELEASING.md.
The two highest-risk design questions were explored with runnable spikes before committing to the build, and both are now settled in the application itself:
- Permissions — Zero expresses multi-workspace read isolation and role-gated writes.
- Notifications — durable at-least-once delivery, a single-leader scheduler, quiet hours, escalation, and acknowledgement from in-app or a channel button. Validated by a test rig that runs real replicas and kills them mid-send. All five channels — ntfy, Telegram, Discord, Slack, and email — deliver.
- Operator-blind attachments — browser-side key enrollment, recovery, workspace grants and forward-only rotation protect file content and names from the server and storage backend; filesystem and S3-compatible ciphertext stores share one authenticated transport.
Both spikes have been removed now that the production code supersedes them. The build proceeds
through a milestone roadmap on develop.
- Development setup and checks, changelog, and release channels
- Security architecture and private security reporting
- Notifications and delivery limits
- Backup and restore, database roles, and trusted proxies
- Data portability and native import format
- Encrypted attachment storage, key-loss recovery, and key rotation
- Android, desktop, native authentication, and native push
- Optional Google push relay and roadmap
Contributions are welcome. See CONTRIBUTING.md for the branch/PR workflow and CODE_OF_CONDUCT.md.
MIT © Ditero Contributors
Self-hosted todo app, local-first task manager, Todoist alternative, shared shopping lists, chore tracker, habit tracker, family organizer, and group task management.
Made by Iulian Dita. Other projects.