A really simple local-first task list. Sync with WebDAV or Google Drive.
Live at dumbtasks.app or self host your own.
The Docker image serves the app with nginx. The self-hosted image syncs with WebDAV only.
-
Get the code:
git clone https://github.com/echus/dumb-tasks.git cd dumb-tasks -
Build and start the container:
docker compose up -d --build
-
Open
http://localhost:8080. -
Put the app behind a reverse proxy with https.
NOTE: Browsers can install the app and use it offline only on https or on localhost.
To show the commit in the sync menu, build with this command:
COMMIT_SHA=$(git rev-parse --short HEAD) docker compose up -d --build- Get the new code:
git pull. - Build and start the container again:
docker compose up -d --build.
Installed apps get the new version the next time they open.
The browser connects directly to your WebDAV server. Thus, the server must allow the origin of
the app (CORS). The origin is the scheme and host of the app address, for example
https://tasks.example.com.
- Install the WebAppPassword app.
- In the admin settings of WebAppPassword, add the app origin to the WebDAV/CalDAV origins.
- In Personal settings → Security, make an app password.
- In the Files app, open Files settings. Copy the WebDAV address, for example
https://cloud.example.com/remote.php/dav/files/you/.
-
Enable
mod_dav,mod_dav_fs,mod_auth_basicandmod_headers. -
Set the lock database in the server configuration, outside all
<Directory>sections. Without it, Apache cannot save files. Debian and Ubuntu set it when you enablemod_dav_fs.DavLockDB /var/lib/apache2/dav/lockdbApache must have write access to the folder of the lock database.
-
Add this configuration to the
<Directory>of the shared folder. Change the origin to the app origin.Dav On AuthType Basic AuthName "WebDAV" AuthUserFile /path/to/htpasswd # Browsers send the CORS check (OPTIONS) without a password <LimitExcept OPTIONS> Require valid-user </LimitExcept> # "always" also sends the headers with errors, for example a wrong password Header always set Access-Control-Allow-Origin "https://tasks.example.com" Header always set Access-Control-Allow-Methods "GET, PUT, DELETE, PROPFIND, MKCOL, OPTIONS" Header always set Access-Control-Allow-Headers "Authorization, Content-Type, Depth" Header always set Access-Control-Max-Age "86400"
-
Restart Apache.
The server must:
- Answer
OPTIONSrequests without a password. - Send the four
Access-Control-*headers above with all responses, also with errors.
- In the app, open the sync menu.
- Select Connect WebDAV.
- Enter the folder address, your username and your password. On Nextcloud, use the app password.
- Select Connect.
The app keeps one file for each device in a dumb-tasks folder at that address.
npm install
npm run dev # http://localhost:5173
npm test
npm run test:servers # WebDAV sync against Apache and Nextcloud, in Docker
npm run test:browser # the production build in Chrome, with the same servers
npm run check # type check
npm run buildThe service worker runs only in production builds: npm run build && npm run preview.
npm run test:servers and npm run test:browser need Docker and the internet (for the
Nextcloud WebAppPassword app). They set up each server as this README says, and take about a
minute the first time. To keep the servers running between runs, set KEEP_SERVERS=1.
npm run test:browser also needs Playwright's Chrome. Install it one time with
npx playwright install chromium. The browser tests do not cover Google Drive, as its sign-in
needs a real Google account. Before a release, test Google Drive sync on staging.
To test WebDAV sync, use a WebDAV server that allows the origin http://localhost:5173. The app
accepts http for a server on this computer.
- Google Drive sync needs a Google OAuth client: Web application, Drive API,
drive.appdatascope. Add each app origin to its JavaScript origins. - Local:
VITE_GOOGLE_CLIENT_IDin.env.local,GOOGLE_CLIENT_SECRETin.dev.vars. - Deploy: a Cloudflare Worker serves the app and the sign-in helper.
wrangler.jsonchas the name and routes. In Workers Builds, setVITE_GOOGLE_CLIENT_IDas a build variable andGOOGLE_CLIENT_SECRETas a secret. - Staging: each push to a branch other than
maindeploys tostaging.dumbtasks.app, the Preview namedstaging.wrangler.jsonchas the Preview command for Workers Builds. Set the Preview'sGOOGLE_CLIENT_SECRETseparately.
Code outside src/lib/ imports it as $lib/....
src/lib/types.ts: the data model (Task,DataFile)src/lib/db.ts: IndexedDB tablessrc/lib/config.ts: the build settings (the Google client, which turns on Drive sync)src/lib/startup.ts: Android share target, persistent storagesrc/lib/tasks/: the task list itselftasks.ts: every task operation (add, move, complete, edit, delete)demo-tasks.ts: the tasks shown on a first visithistory.svelte.ts: undo / redodeadline.ts: finds deadlines in task text ("by friday", "14 nov")guess-icon.ts: picks an icon from keywordscircle-colours.ts: the fallback emoji, and each task's circle colourrecolour.ts: brings older tasks up to the current icon and colour rules
src/lib/sync/: backup and syncmerge.ts: per-field merge for backup import and syncdata-file.ts: JSON export / importengine.ts: pull, merge, push against anySyncBackendlocal-changes.ts: tells sync a task was writtendrive.ts,google-auth.ts: the Google Drive backend and sign-inwebdav.ts: the WebDAV backenddevice-files.ts: what both backends do alike with the device filessettings.ts: what this device keeps about syncing (service, sign-in, progress)sync.svelte.ts: when to sync, connecting each service, and the state the sync menu shows
src/lib/ui/: the parts of the UI that are not componentsdrag-and-drop.ts: dragging a task, from press to drop, withdrag-layout.ts(where it lands),drag-preview.ts(the copy under the pointer) anddrag-scroll.tscaret.ts: puts the caret where a task's text was clickedstate.svelte.ts,theme.svelte.ts,icons.ts: shared UI state, theme, icons
src/components/: the UI (Svelte 5)sync-menu/: the sections of the sync and backup menu (SyncMenu.svelte)
worker/index.ts: the sign-in Worker, which keeps people signed in to DriveDockerfile,docker-compose.yml,docker/nginx.conf: the self-hosted image (WebDAV only)tests/: the tests, in the same layout assrc/helpers/: fake servers, fake Google sign-in, and tabs of the app on one deviceservers/: the WebDAV servers fornpm run test:serversandnpm run test:browserbrowser/: the tests fornpm run test:browser: tasks, offline, drag and drop, sync
Copyright © 2026 Varvara Efremova, under the AGPL-3.0. The license does not cover the name "Dumb Tasks" or the app icon. Use a different name and icon for a fork.