WhatWhen — track when you last did anything WhatWhen track when you last did anything

A minimalist self-hosted app that turns recurring tasks into smart buttons — each one a live timer counting up from its last reset. No database. No auth. No complexity.

No database

Stores everything in a flat JSON file. Mount /data as a volume and data persists across container restarts automatically.

No auth required

Designed to run locally or behind a reverse proxy. Lock it down at the network layer — WhatWhen stays simple and stays out of the way.

Single Go binary

Built on the Go standard library — zero third-party modules. Statically compiled, runs in a scratch Docker image, up in one command.

Clean by design

Light and dark themes — set by your OS preference or toggled manually.

WhatWhen in light mode — a grid of task buttons each showing a timer counting up from the last reset
Light mode
WhatWhen in dark mode — the same task grid with a dark background and lighter text
Dark mode

Get running in seconds

Docker is the fastest path. No build step, no dependencies.

Create a compose.yaml

services:
  whatwhen:
    image: ghcr.io/markmork/whatwhen:latest
    container_name: whatwhen
    restart: unless-stopped
    ports:
      - "8080:8080"
    environment:
      # Override the listen port or data file location if you like.
      - PORT=8080
      - DATA_FILE=/data/whatwhen.json
    volumes:
      - whatwhen-data:/data

volumes:
  whatwhen-data:

Start it

docker compose up -d

Open it

Visit http://localhost:8080 and add your first task.

Run from source (requires Go 1.22+)
git clone https://github.com/markmork/whatwhen
cd whatwhen
DATA_FILE=./whatwhen.json go run .

How it's built

Intentionally boring. The stack is a feature.

Go standard library only
No third-party modules. The HTTP router uses Go 1.22's built-in method + path patterns. The binary is CGO-disabled and runs in a scratch image — minimal attack surface, tiny image size.
Timestamps, not timers
The server only stores a lastReset UTC timestamp. The browser computes elapsed time and ticks once per second. Timers stay accurate across server restarts and closed tabs.
Atomic persistence
Every write goes to a temp file first, then os.Rename atomically replaces the data file. The JSON file is never half-written — no data loss on crash or power failure.
Vanilla frontend
HTML, CSS, and JavaScript — no framework, no build step. Embedded directly into the binary with //go:embed. Theme and lock state live in localStorage, not the server.