Skip to content
iulianditaPublic

About

Self-hosted, local-first tasks for families, friends and small groups. Shared todo and shopping lists, chores, habits and reminders. Offline-capable, multilingual and free.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

346 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ditero

Shared tasks for the people you share life with.

CI License: MIT Status: beta

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.1 is the first beta being prepared, a 0.x prerelease with breaking changes possible before v1.0.0. It is not stable. Until beta publication completes, the latest published prerelease remains v0.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.

Made for shared days

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.

Available on develop

  • 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.

Tech stack

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

Run it (Docker Compose)

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 up

The 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.

Configuration

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_URL from /api/config at 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.

Bundled vs. external Postgres

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-cache

Reuse 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.

Notifications

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

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.

Browser installation and native development apps

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.

Container distribution

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.

Project status

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.

Documentation

Contributing

Contributions are welcome. See CONTRIBUTING.md for the branch/PR workflow and CODE_OF_CONDUCT.md.

License

MIT © Ditero Contributors

Find Ditero

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.

About

Self-hosted, local-first tasks for families, friends and small groups. Shared todo and shopping lists, chores, habits and reminders. Offline-capable, multilingual and free.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages