Skip to content
maxkuengPublic

About

A self-hosted habit tracker and to-do list for people who want the mechanics of Habitica without the role-playing game

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

karotto

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.

Screenshots

Web task board, Tokyo Night Web task board, Synthwave '84
Web, Tokyo Night Web, Synthwave '84
Web task editor, Tokyo Night Web settings with theme picker, Synthwave '84
Task editor Settings and themes
Android habits, Tokyo Night Android dailies, Synthwave '84 Android task editor, Tokyo Night
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.

Layout

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

Development

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 test

Themes 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 app

Android app

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

Every 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 tests

Debug 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.0 in .env and use http://<machine-ip>:3210. Cleartext HTTP is allowed by the app for that purpose; put the deployed server behind HTTPS.

API

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/up

Task 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/events

Command-line client

karotto 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/karotto

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

Server administration

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

Production

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/Zurich

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

Releases and compatibility

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.

Credits

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.

Vibe coded

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.

License

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.

About

A self-hosted habit tracker and to-do list for people who want the mechanics of Habitica without the role-playing game

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages