A self-hosted habit tracker and to-do list for people who want the mechanics of Habitica without the role-playing game. Habits, dailies and to-dos; each task carries a value that drifts up when you do it and down when you don't, and its colour shows where it stands. Streaks, counters, repeat schedules, checklists, tags, reminders and a day rollover at the hour you choose. No HP, XP, gold, items, avatars or popups.
Web app, native Android app, command-line client, MCP server for AI agents, and a Home Assistant integration, all talking to one small API you run yourself.
![]() |
![]() |
| Web, Tokyo Night | Web, Synthwave '84 |
![]() |
![]() |
| Task editor | Settings and themes |
![]() |
![]() |
![]() |
| Android, habits | Android, dailies | Android, task editor |
See docs/DESIGN.md for what was kept, what was dropped and where karotto
deliberately deviates from Habitica. docs/habitica-analysis/ holds the source
analysis the implementation was derived from. Credits and licences for
everything borrowed are in THIRD_PARTY_NOTICES.md.
| Package | What |
|---|---|
packages/core |
Shared zod schemas and pure domain logic: scheduling, scoring, rollover, colours |
packages/server |
Fastify API, Drizzle/Postgres, auth, CLI |
packages/web |
Vite + SolidJS + Tailwind client |
packages/android |
Native Kotlin + Jetpack Compose app, UI modelled on the Habitica Android client |
packages/cli |
karotto, the command-line client for the API, JSON output for scripts and agents |
skills/karotto |
Agent skill teaching the karotto CLI |
karotto-ha-integration |
Home Assistant integration, its own repository so HACS can install it. See docs/HOME_ASSISTANT.md |
Requirements: Node 24, Docker (for Postgres).
npm install
docker compose up -d db
cp .env.example .env # defaults work with the compose database
npm run cli -w @karotto/server -- migrate
npm run cli -w @karotto/server -- user create max --timezone Europe/Zurich
npm run dev # API on :3210, web on :5173 (proxies /api)Checks:
npm run typecheck
npm run lint
npm testThemes live in packages/core/src/theme/themes.ts. After editing them,
regenerate the outputs both clients ship:
npm run theme:css -w @karotto/web # Tailwind variables for the default theme
npm run themes:android -w @karotto/core # Kotlin palettes for the Android appInstall the signed APK from the latest
android-v* release. Point
Obtainium at this repository to get
updates. On first start enter your server URL, username and password; the app
creates its own API token and never stores the password.
Phones with Android's Advanced Protection mode on refuse to install APKs from anything but an app store, Obtainium included. Install over USB instead, which that mode does not block:
adb install -r karotto-<version>.apkEvery release is signed with the same key, so this updates in place and keeps you logged in.
packages/android is a standalone Gradle project (not an npm workspace). It
talks to the same API with a long-lived token created through
POST /api/v1/auth/token from the login screen, caches tasks in Room, scores
optimistically with an offline queue, computes daily due-ness locally and
delivers reminders as exact alarms.
Building it yourself needs JDK 21 and an Android SDK with platform 37. The Gradle wrapper fetches everything else.
cd packages/android
./gradlew installDebug # build and install on the connected device/emulator
./gradlew testDebugUnitTest # scheduling/scoring/day-context testsDebug builds install as a separate app, "karotto dev" with the id
com.karotto.app.debug, so they sit next to the released app instead of
replacing it.
Releases are cut by tagging: git tag android-v0.2.0 && git push --tags.
The Android release workflow builds a signed APK, derives the version from
the tag and attaches the file to a GitHub release. Signing needs the four
ANDROID_* repository secrets (base64 keystore, its password, key alias, key
password); without a keystore a local assembleRelease produces an unsigned
APK.
The app shows a banner when the server is older than the API revision it was built against; see Releases and compatibility.
Server URL on the login screen:
- Emulator:
http://10.0.2.2:3210(the host's loopback). - Phone on the same LAN or Tailscale: start the API with
HOST=0.0.0.0in.envand usehttp://<machine-ip>:3210. Cleartext HTTP is allowed by the app for that purpose; put the deployed server behind HTTPS.
Everything is under /api/v1. Interactive docs are served at /api/v1/docs
(Swagger UI generated from the zod schemas), the raw spec at
/api/v1/openapi.json. Create a
long-lived token in Settings or with the CLI and send it as a bearer token:
npm run cli -w @karotto/server -- token create max --name scripts
curl -H "Authorization: Bearer krt_..." http://localhost:3210/api/v1/tasks
curl -X POST -H "Authorization: Bearer krt_..." -H 'content-type: application/json' \
-d '{"type":"todo","text":"Buy carrots","alias":"carrots"}' http://localhost:3210/api/v1/tasks
curl -X POST -H "Authorization: Bearer krt_..." http://localhost:3210/api/v1/tasks/carrots/score/upTask ids and aliases are interchangeable in URLs. Day rollover is triggered by
clients (POST /api/v1/cron), exactly like Habitica; scripts that score today's
tasks before the web app has been opened should call it first. GET /api/v1/cron/status tells you whether a rollover is pending and which dailies
were due yesterday.
Live updates: GET /api/v1/events is a Server-Sent Events stream of changes to
the caller's data (task.upserted, task.deleted, tasks.reordered,
tasks.invalidated, tags.changed, user.updated). Send an X-Client-Id
header on mutating requests and the same value is echoed as origin on each
event, so a client can skip its own changes. The web app and the Android app
subscribe while open; a wall dashboard only needs the web app.
curl -N -H "Authorization: Bearer krt_..." http://localhost:3210/api/v1/eventskarotto is a separate client for humans, scripts and AI agents. It only
speaks to the API; the server's own maintenance commands are karotto-admin.
Each cli-v* release attaches it as a single executable that needs only Node 24:
curl -fsSL https://github.com/maxkueng/karotto/releases/download/cli-latest/karotto -o ~/.local/bin/karotto
chmod +x ~/.local/bin/karottokarotto update installs the newest release your server supports (see
Releases and compatibility), stepping back to an
older one if the server is behind. Interactive commands mention when a fitting
release is out, checking GitHub at most once a day; KAROTTO_NO_UPDATE_CHECK=1
silences that. Install it somewhere you own, like ~/.local/bin, so it can
update itself; a system-wide copy needs sudo karotto update. The Docker image
ships it too and is updated with the image.
karotto login --url https://karotto.example.ts.net -u max # stores a token in ~/.config/karotto
karotto tasks list --due
karotto tasks add todo "Buy carrots" --due 2026-10-03 --tag errands --alias carrots
karotto done carrots
karotto cron status --json
karotto pause # vacation mode: no penalties until `karotto resume`Every command takes --json. KAROTTO_URL and KAROTTO_TOKEN override the
stored login for scripts. karotto mcp runs the same operations as a Model
Context Protocol server over stdio for agents that speak MCP; register it as
{ "mcpServers": { "karotto": { "command": "karotto", "args": ["mcp"] } } }The deploy script installs it next to the server; elsewhere build it with
npm run build -w @karotto/cli and run packages/cli/dist/main.js.
skills/karotto/SKILL.md is a drop-in skill for agents that can run shell
commands.
Run on the server (the deploy script installs it as karotto-admin):
karotto-admin migrate
karotto-admin user create <username> [--password-stdin] [--timezone <IANA>]
karotto-admin user list | password <username> | delete <username>
karotto-admin token create <username> --name <label> [--expires <iso>]
karotto-admin token list <username> | revoke <username> <id>
In development run it through npm run cli -w @karotto/server -- <args>; in
production, docker compose exec app karotto-admin <args>.
One Docker image, ghcr.io/maxkueng/karotto for amd64 and arm64, plus
Postgres, in docker/compose.yaml:
curl -fsSLO https://raw.githubusercontent.com/maxkueng/karotto/master/docker/compose.yaml
curl -fsSL https://raw.githubusercontent.com/maxkueng/karotto/master/docker/env.example -o .env
$EDITOR .env # set POSTGRES_PASSWORD
docker compose up -d
docker compose exec app karotto-admin user create max --timezone Europe/ZurichRequirements, configuration, backups, updates and how to reach it over Tailscale or a reverse proxy: docs/DEPLOY.md.
Plain http://<machine>:3000 on the LAN works too, but sends passwords and
session cookies unencrypted. Only do that on a network you trust; see
Plain HTTP on the LAN.
Server, CLI and Android app release independently, each from its own tag:
| Tag | Builds | Version from |
|---|---|---|
server-v* |
Docker image (server and web UI) | packages/server/package.json |
cli-v* |
Standalone karotto, cli-latest |
packages/cli/package.json |
android-v* |
Signed APK | the tag |
Bump the package version, merge, then tag, e.g.
git tag cli-v0.4.1 && git push origin cli-v0.4.1. A workflow refuses a tag
that does not match its package.
They stay compatible through the API revision in
packages/core/src/api-revision.json, not through version numbers. The
revision goes up whenever the API gains something; breaking changes need a new
/api/v2 instead. The server sends its revision on every response
(X-Karotto-Api-Revision), and every client build records the revision it was
built against as the minimum it needs. A client that finds an older server
says so instead of having new fields silently ignored, and karotto update
only installs builds the server supports.
A server test fingerprints the generated OpenAPI spec, ignoring descriptions
and summaries. When it fails, the API changed: bump revision and paste the
new fingerprint from the failure message. Changes that leave the API alone
need no revision bump, so a server release does not make clients update, and a
client release does not need a server deploy.
karotto is a de-gamified reimplementation of Habitica
by HabitRPG, Inc. The task model, scoring formulas, day rollover, schedules,
value colours, the optional "Classic" palette and the overall task UX are
Habitica's, reimplemented in new code after a close reading of their GPL-3.0
source. None of Habitica's artwork is used and karotto is not affiliated with
HabitRPG. The colour themes come from Tokyo Night, Synthwave '84 and
synthwave-hass, Catppuccin, Nord, Gruvbox and Solarized. Full attributions
and licences: THIRD_PARTY_NOTICES.md.
This repository was written entirely by an AI coding agent (Claude, via Claude Code) directing itself from conversational instructions. The human involvement was deciding what to build, testing it, reporting bugs and giving feedback on look and feel, and never typing code. Read it with that in mind: it is tested and it works, but no line of it has been through a human code review.
GNU General Public License v3.0, see LICENSE. karotto inherits the licence
from Habitica, whose mechanics it reimplements. Use it, change it, host it,
share it; keep the source open when you do.






