micro-kaiten — a self-hosted personal kanban canvas (Deno + SQLite, Vite/TS, REST API)
3.6K
A self-hosted personal kanban canvas — every board laid out in one spatial area instead of one board at a time. Capture a thought in a keystroke, advance it a column at a time, pan across the canvas. Single-user, no accounts, no cloud.
docker compose up # → http://localhost:8787
That's the whole thing: one Deno + SQLite container serving the API and a dependency-light Vite/TypeScript frontend. Keyboard-first throughout.

Why it looks and behaves this way:
DESIGN.md ·PRODUCT.md. Useful? Support it via ❤️ GitHub Sponsors or Patreon.
N new card · ⇧N new board · A advance ·
J/K move · ⌘K palette · / search. The top-bar New ▾ menu mirrors it for the mouse.→ <board> hint.⌘K.og:title/<title>) and YouTube (oEmbed). Existing URL-titled cards backfill.server/API.md.One container (Deno serves the API and the built frontend on port 8787), pulling the image CI publishes to Docker Hub:
docker compose up # pulls umagistr/mk
docker run -p 8787:8787 -v mk-data:/data umagistr/mk # or run it directly
The SQLite database persists in the mk-data volume (/data/mk.db). Override the port with
MK_PORT. To build from source instead, uncomment build: . in docker-compose.yml (or
docker build -t mk .).
micro-kaiten ships single-user / localhost by default with no auth. To put it on the internet, set a login passphrase:
MK_AUTH_SECRET="a-long-random-passphrase" # >= 16 chars; enables the login gate
MK_API_TOKEN="another-random-token" # optional: bearer token for automation
With MK_AUTH_SECRET set, the browser shows a login screen (session stored in an
HttpOnly + Secure + SameSite=Strict cookie that slides — you stay logged in for up
to ~400 days of continued use) and scripts authenticate with
Authorization: Bearer <MK_API_TOKEN>. See server/API.md.
Log out from the avatar (top-right) → Log out, which ends the session and
returns to the login screen (DELETE /api/session under the hood). An expired session
shows the login screen again automatically.
Hardening notes for a public deployment:
MK_STATIC set) refuses to start without MK_AUTH_SECRET,
so you can't accidentally expose an open server.expose: "8787"
over a published ports: mapping so the proxy is the only ingress, and set
MK_PROXY_DEPTH to the number of trusted proxy hops (default 1) so login
rate-limiting reads the real client IP.deno user. A fresh mk-data volume inherits
the right ownership; an existing root-owned volume needs a one-time
chown -R 1000:1000 (or recreate it) so the database stays writable.Image tags (umagistr/mk): latest = newest release · YYYY.MM.DD.N = a specific
CalVer release (e.g. 2026.06.22.1). Every push to main cuts a
CalVer release via the Release (CalVer) workflow — no SHA-tagged images; each release
builds the image and opens a matching GitHub release.
Two processes — the Vite dev server (frontend, proxies /api → the backend) and the
Deno + SQLite API:
npm install
deno task server # API on :8787 (writes ./mk.db)
npm run dev # app on :5173
Scripts: npm run gate (typecheck + unit tests) · npm run build (production frontend →
dist/) · npm run e2e (Playwright) · deno check server/main.ts.
src/ frontend (canvas, render, store, core reducer/ops, calendar, …)
src/core/ pure domain: state reducer, ops, due/done/archive logic (shared with the server)
server/ Deno + SQLite API + static file serving (API.md = reference)
tests/ vitest unit tests e2e/ Playwright scenarios
The browser store and the server run the same src/core reducer, so the app and the
API can never disagree about what a change means.
Vite · TypeScript · hand-rolled CSS (OKLCH) · Deno · node:sqlite. No UI framework.
Localhost-first; optional single-user auth (passphrase + API token) for public exposure.
Content type
Image
Digest
sha256:9c37060c7…
Size
72.1 MB
Last updated
2 months ago
docker pull umagistr/mk