Waka Personal is a self-hosted, single-user WakaTime-compatible backend with a built-in dashboard. It lets you keep using WakaTime clients and plugins, store the data in PostgreSQL, and browse your own coding activity from a local web UI.
Good fit if you want to:
- keep your coding history in your own database
- point WakaTime-compatible plugins at your own API
- run a personal dashboard on localhost or your own server
- import old backup exports and keep everything in one place
- WakaTime-compatible ingest and query endpoints for heartbeats, durations, summaries, stats, status bar, and file experts
- Built-in Astro + React dashboard, served by the Go app from
dist/ - PostgreSQL storage with
goosemigrations - Backup importer for
.jsonand.json.gzexports - Optional API key auth for a single-user setup
- Health endpoints, rate limiting, and security headers out of the box
- Backend: Go 1.26, Fiber v2, pgx/v5, goose, zerolog
- Frontend: Astro, React, Tailwind CSS
- Database: PostgreSQL
cmd/ app entrypoints
internal/http/ Fiber handlers and transport concerns
internal/service/ business logic
internal/store/ PostgreSQL access and migrations
src/ Astro + React dashboard
db/migrations/ goose migration files
- Copy
.env.exampleto.envand adjust values. - Start PostgreSQL:
docker compose up -d postgresSet DATABASE_URL in .env to match the Docker database name if you use the bundled Compose file:
DATABASE_URL=postgres://postgres:postgres@localhost:5432/dvgamerr?sslmode=disable- Install frontend dependencies and build the dashboard:
bun install --frozen-lockfile
bun run build:dist- Start the API:
go run ./cmdThe API applies pending goose migrations on startup.
If dist/index.html exists, the Go server also serves the dashboard from the same origin.
POST /api/v1/users/current/heartbeatsPOST /api/v1/users/current/heartbeats.bulkGET /api/v1/users/current/heartbeats?date=YYYY-MM-DDDELETE /api/v1/users/current/heartbeats.bulkGET /api/v1/users/current/durationsGET /api/v1/users/current/summariesGET /api/v1/users/current/statsGET /api/v1/users/current/statusbar/todayPOST /api/v1/users/current/file_expertsGET /api/v2/dashboard(dashboard aggregate endpoint, no auth)GET /api/v2/live(live dashboard slice, no auth, used by the web UI every 60 seconds)GET /api/v2/insights(insights page data, no auth)GET /api/v2/wrapped(annual wrapped data with backend-calculated Activity Matrix metrics, no auth)
{
"wakatime.apiKey": "your-local-api-key",
"wakatime.apiUrl": "http://localhost:8080/api/v1"
}[settings]
api_url = http://localhost:8080/api/v1
api_key = <APP_API_KEY>
debug = falseThis is useful when you want to send heartbeats to both Waka Personal and WakaTime (or Wakapi). When api_key is set, heartbeats always go to the primary api_url, and also to every matching pattern in [api_urls]:
[settings]
api_url = http://localhost:8080/api/v1
api_key = <APP_API_KEY>
debug = false
[api_urls]
.* = https://api.wakatime.com/api/v1|<api_key_waka>
.* = https://wakapi.dev/api|<api_key_wakapi>Each endpoint authenticates with its own key: api_key in [settings] must be the key for the primary api_url (here APP_API_KEY), and every entry in [api_urls] carries its own key after the |. Putting your wakatime.com key in [settings] while api_url points at Waka Personal makes the server reject every heartbeat, and they silently pile up in the offline queue.
Prefer Waka Personal as the primary api_url. The status bar and wakatime-cli --today read from the primary endpoint's /statusbar/today, and wakatime.com excludes the AI Coding category from its status bar total — so with wakatime.com as primary, the status bar can show empty even though heartbeats are syncing fine. Waka Personal counts all categories, so keeping it primary makes the status bar reflect your real total.
Use the same value for APP_API_KEY in .env and in your client config.
WakaTime plugins send the API key as the Basic-auth username, and this server only checks that the received value matches APP_API_KEY.
If the frontend and backend share the same origin, leave PUBLIC_API_BASE empty.
Use these variables when building the dashboard:
PUBLIC_API_BASEfor the API origin when frontend and backend are on different originsPUBLIC_APP_TIMEZONEfor dashboard queries
The built-in dashboard uses GET /api/v2/dashboard for the selected range and polls GET /api/v2/live every 60 seconds for live widgets. /insights and /wrapped load from GET /api/v2/insights and GET /api/v2/wrapped; the Wrapped response includes its backend-calculated annual Activity Matrix. These web UI endpoints skip API key auth.
Supports backup files in .json and .json.gz format with top-level user, range, and days[].heartbeats[].
go run ./cmd/importer --file E:\path\to\backup.jsonOptional flags:
--format backup-json--dry-run
On Windows, building or running the DuckDB-backed importer from source requires CGO_ENABLED=1 and a GCC toolchain such as MSYS2 UCRT64.
go run ./cmd/migrate up
go run ./cmd/migrate status
go run ./cmd/migrate down
go run ./cmd/migrate create add_new_table sql
go run ./cmd/migrate fix
go run ./cmd/migrate validateMigration config comes from .env, including DATABASE_URL, MIGRATION_DIR, and GOOSE_TABLE.
go run ./cmd
go run ./cmd/migrate up
go run ./cmd/importer --file <path-to-backup.json>
gofmt -l cmd internal
go test ./...
go vet ./...
go mod verify
go mod tidy -diff
bun run check
bun auditbun run check runs the repository-wide Prettier check and produces all four
static Astro routes in dist/. The Go suite covers the HTTP, service, store,
and domain packages; PostgreSQL integration tests run when their database
configuration is available and otherwise skip safely. A .golangci.yml is
included for environments that have golangci-lint installed.
The dashboard build intentionally shares one environment-derived client config
across all Astro pages. Keep API base and timezone normalization in
src/lib/dashboardConfig.js rather than duplicating it in page frontmatter.
On Windows, avoid rebuilding over the same running .exe path. go build -o bin/waka.exe ./cmd/main.go will fail while bin/waka.exe is still running because Windows locks the file.
Use one of these instead:
go run ./cmd
.\scripts\dev-api.ps1
.\scripts\dev-api.ps1 -NoRun.\scripts\dev-api.ps1 builds a timestamped bin/waka-dev-*.exe, removes old stopped dev binaries, and runs the new build without colliding with the previous process.
- Auth is bypassed when
APP_API_KEYis empty, so set it in production. - If recent activity looks missing, check for heartbeats stuck in the client's offline queue:
wakatime-cli --offline-count, then flush withwakatime-cli --sync-offline-activity 100. - The default config database name is
waka_personal, butdocker-compose.ymlusesdvgamerr. - When changing heartbeat persistence or import behavior, check both the live ingest path and the importer path.
- The dashboard and live endpoints intentionally run independent data reads in parallel. Preserve their partial-result response shape and deterministic
errorsordering when changing that orchestration. - The interface uses sharp corners. Do not add Tailwind
rounded-*utilities; the global radius token is zero by design.