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 preparation.
0.1.0-beta.1is the first beta being prepared, a0.xprerelease with breaking changes possible beforev1.0.0. It is not stable. Until beta publication completes, the latest published prerelease remainsv0.0.1-alpha.12. The changelog describes the candidate 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 CSV, Todoist project CSV snapshots and plain Trello board JSON imports; 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
iOS, TickTick and Microsoft To Do import remain pending. Wider attachment migration runtime, terminal, native and deployment qualification remain pending. Voice capture is pending final qualification. 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. Returning after an interval expires records that interval once and advances according to the auto-cycle setting; 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. After reload, elapsed time relies on the device clock. Moving that clock forward can count unworked time, capped at the interval length.
| 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 ghcr.io/iuliandita/ditero:nightly and :nightly-zero. Add --build
to build from this checkout instead. Set DITERO_IMAGE_TAG=0.0.1-alpha.12
to run the latest published prerelease with matching app and Zero tags until beta
publication completes. 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. |
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, an Alpine app default, and a Debian app variant
when a release is cut. :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.