A local-first to-do list with native apps for macOS and Android, a self-hosted web interface, and sync through storage you already own: a WebDAV folder, a CalDAV server or a plain directory. No account, no service to subscribe to.
Lists covers the part of 2Do that a single person uses every day. A task has a list, dates, a priority, tags, a repeat, a reminder, notes in Markdown, attachments and subtasks, which are plain lines with a title; tasks group into projects, and saved filters cut across the lists. A task is closed as done or as "won't do", and stays in view for a few seconds before it leaves. Attachments open inside the app, can be saved or shared, and work in the web interface. The apps for macOS and Android show the calendar events of the day in Today, remind with a sound you choose and keep automatic local backups. Everything works offline; edits made on two devices to different fields of the same task are both kept.
The interface is in English with a Russian localization and follows the system language.
The pictures show the same demo data on every platform; cargo run --example demo -- <empty folder> in core/ makes it.
| Today | A list | A task |
|---|---|---|
![]() |
![]() |
![]() |
| Quick entry | Lists and views | A filter | Settings |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
| Today on a phone | A task on a phone |
|---|---|
![]() |
![]() |
Every release on the Releases page carries the macOS disk image, the Android APK and a SHA256SUMS file; the web server is published as a Docker image.
Needs macOS 14 or newer on Apple silicon.
- Download
Lists-<version>-macos-arm64.dmgand open it. - Drag Lists to Applications and start it from there.
A release whose notes say the build is not notarized is refused by macOS on first start. Remove the quarantine flag once:
xattr -dr com.apple.quarantine /Applications/Lists.appThe share extension works only in notarized builds and in builds made with your own certificate.
Needs Android 8.0 or newer on an arm64 device, which is every current phone.
- Download
Lists-<version>-android.apkon the phone. - Open the file and allow the browser or file manager to install apps when Android asks.
Every release is signed with the same key, so a newer APK installs over the older one and keeps the data. The SHA-256 fingerprint of the signing certificate is 4aca5869f8c3b2ffc45fa2b62530d5c15fb6790fcc37bcc9c94e3b170cf63c09; apksigner verify --print-certs shows it.
The image ghcr.io/juev/lists is built for amd64 and arm64. The server speaks plain HTTP, so the password and the session cookie travel in clear text: publish the port on the loopback address only and reach it from elsewhere through a reverse proxy that terminates TLS.
docker run -d --name lists \
-p 127.0.0.1:8080:8080 \
-v lists-data:/data \
-e LISTS_WEB_PASSWORD='choose a password' \
-e LISTS_TRUST_PROXY=1 \
ghcr.io/juev/lists:0.8.1Open http://127.0.0.1:8080 on the same machine and sign in with the password. The reverse proxy, sync, OpenID Connect and a Compose file are covered in docs/self-hosting.md. Run the version of the latest release, the same as the apps: a server older than 0.5.1 stops syncing with a storage the current apps write, as Sync explains. Attachments are streamed to and from the disk and served with byte ranges; the server takes files up to 512 MiB and 2 GiB per task by default, which LISTS_MAX_FILE_MB and LISTS_MAX_TASK_MB change. Scripts reach it with API tokens made in the web interface; the API is described in docs/api.md.
- Add a task: ⌘N on macOS, the "+" button on Android, the New task field in the web interface. Type it and press Enter. One line is enough:
report friday 10:00 !! #work @Projectssets the due date, a medium priority, a tag and the list. A tag is one word of up to 196 bytes (196 Latin letters or 98 Cyrillic ones); a longer tag is refused with an error. - Open Settings → Sync on each device and point them all at the same WebDAV folder or CalDAV server. Until then the data stays on the device, and the app is fully usable.
- Coming from another task manager? Import a 2Do backup, a Todoist CSV, a TickTick backup, an OmniFocus CSV, a Things database, a Trello board or Microsoft To Do lists: File → Import… on macOS, Settings on Android, the sidebar in the web interface. Leaving goes the other way: everything exports to JSON in a documented format, or to Markdown for reading.
The user guide describes lists, tasks and attachments, subtasks and projects, repeats, quick entry, filters, sync, notifications, calendar events, keyboard shortcuts, backups, export and import.
| Kind | What is stored | When to pick it |
|---|---|---|
| WebDAV or folder | the app's own change log, full fidelity | any WebDAV server, a mounted share, a folder synced by another tool |
| CalDAV | one VTODO per task, a calendar per list | tasks should also be visible in other CalDAV clients |
With CalDAV what the app knows about a task travels in a custom property next to the standard ones (the shared settings and attachments over 5 MB do not travel), edits made by other clients are picked up, and their own properties are preserved. Servers that strip unknown properties reduce it to the standard fields. Details: docs/specs/caldav.md.
Lists 0.5.0 moves a WebDAV or folder storage to format 2, with compressed snapshots; earlier versions stop syncing with such a storage and say so. Lists 0.5.1 and later move the storage from lists/v1/ to lists/, and 0.5.0 stops syncing with a moved storage in the same way. Update every device and the web server together. CalDAV is not affected.
A WebDAV or folder storage can be encrypted with a passphrase from Settings → Sync on a Mac or a phone (usage); earlier versions stop syncing with an encrypted storage and say so, so update every device first. CalDAV is not encrypted. The storage password and the passphrase are kept in the macOS keychain and, on Android, encrypted with a key from the Android Keystore; they are never written to the database.
| Document | What it covers |
|---|---|
| docs/usage.md | using the apps and the web interface |
| docs/self-hosting.md | running the web server: Docker, settings, sign-in, reverse proxy |
| docs/releasing.md | cutting a release, signing keys and repository secrets |
| docs/architecture.md | design decisions and their reasons |
| docs/android-design.md | Android theme, measures, colours and screenshot tests |
| docs/android-visual-direction.md | the look of the Android app: the directions compared and the one chosen |
| docs/specs | requirements, data model, sync rules, CalDAV mapping, import |
| Path | What |
|---|---|
core/ |
Rust crate: storage (SQLite), merge of concurrent edits, sync, recurrence, quick-entry parsing |
apple/ |
macOS app, share extension and the helper of quick entry: SwiftUI, with the task list, the cards and the sidebar in AppKit |
android/ |
Android app, Kotlin and Jetpack Compose |
web/ |
lists-web: a small server with the same core and a web interface |
scripts/ |
builds the core for each platform and generates the bindings |
The apps reach the core through UniFFI bindings generated at build time; generated files are not committed. The Rust version is pinned in rust-toolchain.toml.
Core and web server, on any machine with Rust:
make test # unit tests, behaviour tests, sync tests, sync over a local WebDAV server
make lint # rustfmt and clippy
make deny # advisories and licenses of the dependencies (brew install cargo-deny)
make web # web/target/release/lists-webmacOS app (Xcode 16 or newer, brew install xcodegen):
make apple # apple/build/Build/Products/Debug/Lists.app
make apple-run
make apple-test # the tests in apple/Tests; they do not start the app
make apple-lint # SwiftLint, settings in apple/.swiftlint.yml (brew install swiftlint)The share extension and the app share one database through an App Group, which needs a signing team. The build takes the team from LISTS_TEAM_ID or from the first "Apple Development" certificate in the keychain. Without a team the app is signed ad hoc and keeps its data in Application Support; the share extension then has no access to it and says so in its form instead of saving.
Android app (JDK 17 or newer, Android SDK with platform 36 and an NDK, rustup target add aarch64-linux-android x86_64-linux-android, cargo install cargo-ndk):
make android # unit tests, then android/app/build/outputs/apk/debug/app-debug.apk
make android-install # onto the connected device or emulator
make android-lint # ktlint, settings in .editorconfig (brew install ktlint)JAVA_HOME and ANDROID_HOME default to the Homebrew and Android Studio locations on macOS; override them in the environment.
Docker image, from the repository root:
docker build -t lists-web .cd core
cargo run --example webdav -- /tmp/lists-dav 8765This serves a folder over WebDAV and CalDAV with user user and password secret. Point the macOS app at http://127.0.0.1:8765 and the Android emulator at http://10.0.2.2:8765. cargo run --example lists is a small command-line tool for looking into a data folder; it takes the password from LISTS_PASSWORD.
To check CalDAV sync against a real server, point the ignored test at it. It creates and removes its own tasks and one calendar, and leaves the inbox calendar behind:
cd core
LISTS_CALDAV_URL=https://example.org/dav/calendars/me/ LISTS_CALDAV_USER=me LISTS_CALDAV_PASSWORD=… \
cargo test --test caldav real_server -- --ignoredVersion 0.8.1. What is implemented and how each part was checked is listed at the end of docs/specs/product.md.
Known gaps:
- no iOS app; the web interface covers the phone;
- a CalDAV storage is not encrypted, and neither is a WebDAV or folder storage until a passphrase is set (Sync);
- the macOS app is built for Apple silicon only, the released APK for arm64 only.
MIT.














