PRODUCTIVITY / FULL-STACK
KodaPomo
Pomodoro Dashboard
A focused productivity dashboard built around an accurate Pomodoro timer, task planning, ambient soundscapes, and session analytics.
Visit KodaPomoAt a glance
- Role
- Solo full-stack engineer โ product, data, and AI surfaces
- Timeline
- 2026
- Platforms
- Web (PWA) ยท Android wrapper ยท Vercel ยท Supabase
- Scope
- Timer, tasks, soundscapes, analytics, sync, subscriptions, AI planner and insights
- State
- Live product
- Read time
- 6 min

Main screen
The focus timer and daily workflow at the centre of KodaPomo.
Why this screen
The primary loop: the timer and the current task in one view.
Engineering highlights
- Drift-resistant timer based on elapsed wall-clock time
- Task compartmentalisation with estimates and session attribution
- Mixable ambient soundscapes on an isolated audio engine
- Daily and weekly analytics with rule-based coaching
- AI planner and AI insights on a quota-bounded pipeline
- Offline-first storage with optional account sync
Why it exists
The initial objective was deliberately small: an online Pomodoro app. Not a task manager with a timer bolted on โ a tool that makes the 25 minutes of focus and 5 minutes of rest of the Pomodoro technique concrete, so a user can compartmentalise a piece of work and manage effort instead of guessing at it. A timer on its own does not make a useful product: the current task has to stay visible, transitions between focus and rest have to be predictable, and enough history has to survive that a user can actually read their own working rhythm.
Where it started
Original objective
Ship an online Pomodoro timer: 25 minutes of focus, 5 minutes of rest, a long break every fourth session โ a place to park one task and work through it in compartments rather than in a single exhausting push.
The technique
The classic 25/5/15 cycle is the baseline, not a constraint: study, writing, programming and design presets re-pace it (programming runs 50/10/20) while the underlying model stays identical, so a different rhythm never means a different code path.
How the scope changed
v0.1.0 โ a generated timer
Started as a generated Next.js Pomodoro UI: one mode, one countdown, no persistence beyond the tab.
v0.2.0 โ architecture overhaul
Split the single component into a composition root plus independent stores for the timer, tasks, settings, statistics and interface, so each concern could evolve without rewriting the shell.
Compartmentalising work
Tasks with pomodoro estimates, an active task bound to each session, per-session attribution, and a soft-delete archive โ the timer stops measuring anonymous time and starts belonging to work.
Soundscapes and licensed music
Five mixable ambient channels on a Howler engine kept outside React, later joined by a licensed music library served from private storage so audio never touches timer accuracy.
Analytics and coaching
Session history becomes streaks, weekly minutes, peak hours and estimate accuracy, with a deterministic rule-based coaching layer that costs nothing to run.
AI planner and AI insights
A goal becomes a structured curriculum (preflight questions โ blocks โ tasks โ subtasks), and completed sessions become an analysis whose confidence is gated by how much evidence exists.
Accounts, sync and monetisation
Supabase auth, per-identity local storage behind a sync engine, Stripe on web and RevenueCat on device โ added without ever making an account a condition for starting a session.
How it is built
KodaPomo is a Next.js App Router application whose dashboard is a single client composition root. Seven Zustand stores own independent concerns โ timer, tasks, settings, statistics, interface, planner, subscription โ and persist to browser storage namespaced per identity, so the app is fully usable offline and without an account. Server-side code is deliberately thin: route handlers for AI, subscriptions, webhooks and signed music delivery, backed by Supabase (Postgres and auth) and Upstash Redis.
Data flow
Modules
Engineering decisions
Derive the timer from elapsed wall-clock time rather than interval ticks
- Why
- Interval callbacks drift when the browser throttles or backgrounds a tab
- Trade-off accepted
- More careful lifecycle handling to reconcile time on focus, resume and rehydrate
Keep the audio engine fully independent of timer state
- Why
- Sound choices must never affect the accuracy of a session
- Trade-off accepted
- A separate engine, lazy import on first gesture, and extra lifecycle coordination
Persist everything locally with no account requirement
- Why
- A focus tool should work immediately and offline; the cloud is a mirror, not the source of truth
- Trade-off accepted
- History is device-bound until the user signs in, and sync has to reconcile two writers
Sync sessions and settings as a full replace rather than a field-level diff
- Why
- Correct and simple for a single-device personal app, and those tables are append-mostly
- Trade-off accepted
- The push is non-atomic (delete then insert), so a partial failure is not yet non-destructive
Serve every AI call through one OpenAI-compatible client
- Why
- Model and endpoint are configuration, so cost pressure is answered by swapping environment values instead of rewriting the pipeline
- Trade-off accepted
- Prompts and output contracts are tuned against one model family at a time
Data decisions
What was hard
Browsers throttle background tabs, which breaks naive interval timers
Importing a generated curriculum froze the main thread
Audio and timer must never influence each other
Reloads and multiple tabs double-recorded completed sessions
Long AI generations were killed by Safari's idle request timeout
AI engineering
The first AI implementation worked and was unsustainable. It consumed credits faster than the product could justify, its answers were not tailored โ it recommended paid resources for everything โ and it would happily be called with a blocked, dangerous or nonsensical prompt because nothing inspected the request before it reached the model. The layer was rebuilt around three ideas: bound the spend, bound the input, and make the context structured.
Token spend scaled with ambition rather than value
- Control
- Per-route output caps from 512 to 16,384 tokens against a hard ceiling, an input budget of roughly 32K enforced as a 28,000-token clamp that trims instead of rejecting, and per-field clamps on every user-supplied string
- Effect
- Every request has a known worst case. The whole-curriculum refinement endpoint, at about 17K input tokens per call, was deleted rather than optimised
Model choice was locked to one vendor's pricing
- Control
- A single OpenAI-compatible client with model and base URL as environment values โ production runs a low-cost flash model, with OpenAI, DeepSeek, OpenRouter, Groq and local Ollama presets ready
- Effect
- Cost pressure is answered with configuration, not a rewrite
Answers were generic and pointed users at paywalls
- Control
- Context presets per discipline, a computed focus history (sessions, streak, peak hours), a curated database of free resources, a paywall domain list, and link checks that drop unreachable or gated URLs
- Effect
- Plans reflect how the user actually works and only cite material that can actually be opened
Any prompt โ gibberish, unsafe or injected โ reached the model
- Control
- Pre-call validation for length, repetition, gibberish and injection patterns, a trilingual safety bank, two-layer moderation, and request schemas that validate every field
- Effect
- Blocked input returns 422 before a single token is spent; borderline input returns 400 with an actionable message the user can fix
Free-form output made results unpredictable
- Control
- JSON-mode responses validated by schemas after a lenient normalisation pass, with a deterministic estimation table when the model omits or misjudges effort
- Effect
- A partial or mislabelled response still renders as a usable plan instead of an error screen
Nothing stopped one client from exhausting the budget
- Control
- Redis quotas with true rolling windows โ 3 a day and 10 a week on the free tier, 30 a day and 120 a week on premium, plus a per-IP ceiling โ failing closed with 503 when the limiter is unavailable
- Effect
- Spend per user is bounded by construction, and the limit never silently disappears when infrastructure misbehaves
Failures surfaced as raw errors
- Control
- One error taxonomy mapped to status codes, retryability and localised toasts, so a rejected goal, a timeout and a schema failure are three different recoverable experiences
- Effect
- Users see a warning toast and an inline retry instead of a stack trace โ and the failure is handled in the interface, not just in the API
Quotas
Guardrails
- Untrusted text is fenced in tagged blocks with an explicit instruction never to follow instructions found inside them
- User-supplied URLs pass an SSRF guard: HTTP(S) only, ports 80 and 443, private and cloud-metadata hosts rejected
- Insights are evidence-gated: confidence is forced low under ten sessions, chronotype and causation claims are forbidden, and the 21-day habit myth is explicitly banned
- Every AI route checks origin, authentication, configuration and quota before a model is called
- Generated plans and insights are cached locally, so a paid result is never bought twice
How it is verified
Correctness is a release gate rather than an afterthought: unit tests over the stores and the AI layer, browser flows, a security suite, and a static quality gate that fails CI when the design system regresses.
- 55 Vitest suites covering stores, timer logic, sync, AI schemas and security
- 16 Playwright end-to-end flows across Chromium, Firefox and WebKit
- Security tests for identity isolation, origin checks, SSRF, rate-limit bypass and input size limits
- Biome, TypeScript and a Capacitor configuration guard on every push to main
- Lighthouse audit with the performance fixes asserted as tests
Where it stands
KodaPomo is live at kodapomo.com as a web PWA with an Android wrapper over the same deployment. It grew from a generated Pomodoro timer into a full product โ sync, subscriptions, licensed audio, and an AI planner whose cost per user is bounded by design. The repository stays private, so the published surface is the product itself and this engineering write-up.
Known limitations
- Sessions and settings sync as a full replace rather than a diff, so a failed push is not yet non-destructive
- Planner data and task extras are still device-local until the sync engine covers them
- The Android wrapper is gated behind device and store checks; signing, closed testing and store listing are open work
- Some pricing copy still advertises 200 weekly AI generations while the enforced premium quota is 120
- The repository is private; only the live product and this write-up are public
Next
The near-term work is premium feature delivery, Android release readiness, and making session and settings sync non-destructive when a push fails.
In short
KodaPomo is the project where I stopped shipping features and started budgeting systems. The interesting work was never the countdown: it was deciding what deserves a table in Postgres, what belongs in browser storage, what a model is allowed to see, and what a single generation is allowed to cost. Those decisions are the engineering โ the timer is only the part you can see.