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

410 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. The latest published prerelease is v0.1.0-beta.5, a 0.x prerelease with breaking changes possible before v1.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.

Ditero's Weekly priorities board with household and garden club tasks grouped by priority

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

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

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