Skip to content

Repository files navigation

K-Mood

Self-hosted mood tracking journal. Log how you feel, then find out what actually moves it.

Features

  • Mood tracking with 5 discrete states (Awful, Bad, Meh, Good, Rad)
  • Activities with custom categories
  • Rich entries with markdown notes, photos, voice memos, location, and what you were listening to
  • Daily metrics — steps, sleep, resting heart rate, HRV, exercise, screen time, alcohol — entered by hand or imported from a phone
  • Correlation between anything measured and your mood, with several methods run side by side; agreement between them is the signal, not any single number
  • Weather resolved from an entry's location, and cycle tracking if you want it, both correlated like anything else
  • Trends including streaks, distribution, and a calendar heatmap
  • Importer tokens so a Shortcut or a cron script can send metrics without your password
  • Import from Daylio CSV or any CSV with a mapping wizard
  • Two exports that cannot be confused: a complete backup for keeping, and a readable journal for sharing
  • Multi-user with JWT authentication and per-user data isolation
  • PWA installable on mobile and desktop
  • Self-hosted with SQLite and local or S3 media storage

Quick Start

Docker

docker run -d --name k-mood \
  -p 3000:3000 \
  -v k-mood-data:/data \
  -e KMOOD_AUTH__JWT_SECRET=your-secret-here \
  ghcr.io/gabrielkaszewski/k-mood:latest

docker run -d --name k-mood-worker \
  -v k-mood-data:/data \
  -e KMOOD_AUTH__JWT_SECRET=your-secret-here \
  --entrypoint k-mood-worker \
  ghcr.io/gabrielkaszewski/k-mood:latest

Open http://localhost:3000, register an account, and start logging.

The second container is the worker. Everything that happens on a schedule lives there — sending reminders, clearing expired sessions, resolving weather from an entry's location, and filling in the identity of songs logged while an upstream was unavailable. Run the server alone and none of that happens; nothing else breaks.

Docker Compose

services:
  k-mood:
    image: ghcr.io/gabrielkaszewski/k-mood:latest
    ports:
      - "3000:3000"
    volumes:
      - k-mood-data:/data
    environment:
      - KMOOD_AUTH__JWT_SECRET=your-secret-here

  k-mood-worker:
    image: ghcr.io/gabrielkaszewski/k-mood:latest
    entrypoint: ["k-mood-worker"]
    volumes:
      - k-mood-data:/data
    environment:
      - KMOOD_AUTH__JWT_SECRET=your-secret-here
    depends_on:
      - k-mood

volumes:
  k-mood-data:

From Source

Requires Rust 1.88+ (the code uses let-chains, stable since 1.88) and Bun.

make run

That builds the frontend, then starts the server with the worker beside it. Stopping the server stops both. make run-server and make run-worker start one at a time, make dev is the same pair with debug logging, and make check runs everything CI would.

Configuration

Three layers, each overriding the one before it: compiled-in defaults, then config.toml, then the environment. Copy config.example.toml to config.toml and adjust as needed — or set nothing at all and run on the defaults.

Section Key Default Description
server host 0.0.0.0 Bind address
server port 3000 HTTP port
server.cors allow_any_origin true CORS policy
auth jwt_secret none JWT signing key. Required — the server refuses to start without it
auth allow_registration true Enable new user registration
storage data_dir ./data SQLite and media storage path
storage.media backend local local or s3
analysis minimum_sample_size 30 Days needed before any correlation is shown
analysis false_discovery_rate 0.10 How often a marked correlation is expected to be a fluke
import maximum_days_per_import 90 Longest health import accepted in one request
import rejections_kept 200 Unusable readings kept per account, newest first
worker look_up_weather true Set false and no coordinates leave the machine
worker most_attempts 5 Failures before a job stops retrying and stays visible
worker sweep_seconds 900 How often stranded work is rediscovered

Every key has a default, so a section you leave out simply uses them. The full set with comments is in config.example.toml.

From the environment

Every key has an environment twin: the prefix KMOOD_, a double underscore for each section you descend, and the key spelled as it is in the file.

KMOOD_SERVER__PORT=8080
KMOOD_AUTH__JWT_SECRET=...
KMOOD_STORAGE__DATA_DIR=/var/lib/kmood
KMOOD_SERVER__CORS__ALLOW_ANY_ORIGIN=false
KMOOD_WORKER__LOOK_UP_WEATHER=false

A single underscore stays inside a key name, so data_dir is written DATA_DIR; a double underscore descends a section. Lists are arrays — KMOOD_SERVER__CORS__ALLOWED_ORIGINS=["https://a.example","https://b.example"] — and the media backend switches the same way, with KMOOD_STORAGE__MEDIA__BACKEND=s3 beside KMOOD_STORAGE__MEDIA__BUCKET and KMOOD_STORAGE__MEDIA__REGION.

Secrets belong here rather than in a file: auth.jwt_secret, push.vapid_private_key, provider.encryption_key, and the S3 access and secret keys.

KMOOD_CONFIG_FILE names a file other than config.toml. Having no config.toml is fine, since the defaults are a complete configuration — but a file you name that does not exist is an error, as are a malformed file and a value of the wrong type. The reasoning is in ADR 0013.

Push Notifications

K-Mood supports Web Push notifications (works on iOS 16.4+ when added to Home Screen, Android, and desktop browsers). No Firebase or third-party service required.

1. Generate a VAPID private key (32 bytes, base64url or standard base64):

python3 -c "
import subprocess, base64
key = subprocess.check_output(
    'openssl ecparam -genkey -name prime256v1 -noout 2>/dev/null | openssl ec -outform DER 2>/dev/null',
    shell=True
)
print(base64.urlsafe_b64encode(key[7:39]).rstrip(b'=').decode())
"

Standard base64 is accepted too: + and / are rewritten and any padding is stripped before use, so a key copied from a tool that emits the standard alphabet works unchanged.

2. Add to config.toml:

[push]
enabled = true
vapid_private_key = "<output from step 1>"
vapid_subject = "mailto:you@example.com"

Or keep the key out of the file entirely:

KMOOD_PUSH__ENABLED=true
KMOOD_PUSH__VAPID_PRIVATE_KEY=<output from step 1>
KMOOD_PUSH__VAPID_SUBJECT=mailto:you@example.com

3. Enable in the app: Go to Settings and tap "Enable" under Notifications. Use "Send test notification" to verify it works.

The worker checks reminders every 60 seconds and sends push notifications to all subscribed devices for users with due reminders. Users must set a timezone in their profile for reminders to fire — and the worker must be running, or nothing is sent.

Architecture

Rust workspace with DDD and hexagonal architecture:

crates/
  domain/          Pure domain logic, entities, value objects, ports
  application/     Use cases as free-standing functions
  api-types/       Request/response DTOs and Zod-like validation
  config/          Configuration types, defaults, and the file/environment layering
  adapters/
    http-axum/     REST API (axum) + SPA serving
    sqlite/        SQLite persistence (sqlx)
    auth/          JWT + Argon2 authentication
    storage/       Media storage (local filesystem / S3)
    event-publisher/ Domain event bus (tokio mpsc)
    importer/      Daylio CSV, generic CSV, and backup reading
    exporter/      Complete backup (ZIP) and shareable journal (Markdown)
    music/         Subsonic now-playing, MusicBrainz recording lookup
    weather/       Open-Meteo lookup, WMO codes mapped to our own vocabulary
    crypto/        Provider credential encryption
    web-push/      Reminder delivery
  bootstrap/       Shared wiring, so both binaries build one object graph
  server/          The HTTP binary (k-mood)
  worker/          The background binary (k-mood-worker)
spa/               React 19 SPA (TanStack Router, shadcn/ui, Tailwind v4)

Two processes, one SQLite file. The server only serves; everything on a timer — reminders, session cleanup, enrichment — is the worker's. Background work is queued but the queue is deliberately losable: nothing is enqueued that a query over stored data cannot rediscover, so a lost job costs promptness and never data.

Decisions with reasoning worth keeping are in docs/adr/, and the domain vocabulary is in CONTEXT.md.

API

Interactive API docs are available at /docs (Scalar UI) when the server is running. The OpenAPI spec is at /openapi.json.

License

MIT

About

Self-hosted mood tracking journal.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages