The problem
Time trackers are usually one of two things: a cloud silo that owns your data, or a dumb stopwatch with no memory of what the time was actually spent on. Notes live somewhere else entirely, a separate app, a separate login, so the record of what you did and the record of what you learned never meet each other.
I wanted one workspace that tracks time, holds notes, and can reason about both, without asking me to trust it with my data by default. Mostly because I didn't trust myself to remember which cloud service I'd signed up for last.
What I built
Koku is a Next.js app that runs entirely against the browser's IndexedDB by default, with an optional cloud mode (sign-in, sync, and a small admin workspace) built on the same codebase:
- Time tracking: a live timer with pause/resume, pomodoro mode, nested child timers, and a break runner that auto-pauses whatever else is running.
- Tasks: a kanban board (open / in progress / paused / done) whose time entries accumulate directly against the task.
- Notes: a TipTap editor with wiki-style
[[links]], tags, and a private personal-notes table that's hard-excluded from every admin view and export. - Knowledge graph: two WebGL graphs (sigma + graphology), one literal, from note links, and one projected, from time entries logged together.
- Reports & AI: monthly analytics with CSV/JSON/PDF/XLSX export, plus bring-your-own-key stand-ups and monthly narrative reports.
Two modes, one codebase
The same build ships as a zero-config local app and as a multi-tenant
cloud app, gated by a single LOCAL_MODE flag.
Your data never leaves the device
Every table lives in IndexedDB via Dexie, read through live queries so every view stays current with no polling. Export everything as one JSON file at any time. No lock-in, no "contact support to get your data back."
- Dexie 4 · 12 tables · schema v6 with migrations
- Zero config, works offline as an installable PWA
- One-click
setup.sh/.command/.ps1self-host - No account, no server, no telemetry by default
Same app, hosted for a team
Flip the flag and the app gains Zoho Catalyst hosted auth, an outbox-pattern sync engine per table, presence tracking, and an admin workspace for user and usage management.
- Catalyst Datastore mirrors every local table
- Revision-based conflict rejection on push
- Admin: user/group management, per-user usage detail
- Personal notes stay excluded even in cloud mode
Design decisions
A few calls I'm happy with:
- Outbox sync with load-bearing table order.
taskspushes beforetimeEntries, so a pushed entry'staskIdnever points at a task the cloud mirror doesn't have yet. Order matters more than you'd think until you get it wrong once. - A separate queue for ephemeral state. Live timer/break
state goes through its own
pendingLiveMutationsqueue, deliberately excluded from manual sync and tombstoned server-side after 24h. - Charts split three ways. Pure transforms
(
lib/charts/segments.ts) feed a centralised theme which feeds presentational components, so the segment maths is fully testable without ever touching a DOM. - Segmented, not aggregated, day bars. A day renders as a stack of segments, one per work log, instead of one flat bar per day.
- Time entries projected into a graph. Entries have no real links to each other, so they're projected into a graph whose edges mean "these were logged together," weighted by hours.
- Cross-tab leader election for notifications. Web Locks plus
a heartbeat lease so three open tabs don't fire three copies of every
reminder. The notification's stable
tagis the actual guarantee against duplicates; the election just stops the duplicate work from happening in the first place. - Bring-your-own-key AI. Keys stay in IndexedDB and are forwarded per request, and the audit logger redacts secrets before anything gets logged.
adoptedStyleSheets
couldn't carry the @import "tailwindcss" that globals.css
opens with, so the styles get transplanted manually instead. The window's lifecycle
also has to live outside React: StrictMode double-invokes effects in dev, and
the transient user activation the PiP API requires gets spent on the first call.
Open it from inside an effect and it'll open, tear itself down, and then simply
refuse to reopen. Ask me how long that one took to track down.
Highlights
- Installable PWA with an offline app shell and live-update detection, open tabs show a refresh icon when a new build ships
- 6 accent themes (terracotta, ocean, forest, lavender, amber, slate), each with a light and dark palette
- Exports to CSV, JSON, PDF, XLSX and iCal
- WebGL knowledge graph (sigma + graphology) with force layout and a side rail
- TipTap editor with wiki-style
[[links]], tags and slugs - Check-in / break / end-of-day notifications with quiet hours and a quick-note composer built into the notification action
- A custom
npm run auditchain fails the build ondangerouslySetInnerHTML,eval(, raw.innerHTML =, or a static import of a heavy lib that should stay lazy - 刻 Icons baked from the glyph at author time, since a browser rasterizing a favicon has no guaranteed CJK font, so the glyph would otherwise collapse into a blank tile