Overview
A small productivity app: write down tasks, run a timer while you work on them, and see where the time went each day and week. A Next.js front end talks to an Express and PostgreSQL API, in one npm-workspaces repo. The feature list is short on purpose — the effort went into the parts that are easy to get subtly wrong.
Problem
Time trackers fail quietly. A browser clock drifts or sleeps, two tabs start two timers, a session runs past midnight or through a daylight-saving change — and the numbers stop adding up. Get any of it wrong and the one thing the product promises, an honest record of time, is gone.
My role
- Designed and built the whole product: Next.js front end, Express API, PostgreSQL schema and deployment.
- Pushed every rule I could into the database — a partial unique index, a composite foreign key and CHECK constraints, hand-written into versioned migrations.
- Wrote the daily and weekly analytics as timezone-aware SQL instead of client-side arithmetic.
- Built cookie-based auth with Argon2id hashing and refresh tokens that rotate on every use.
- Added an optional AI helper that turns a rough note into a clean task title and description, with Gemini or Claude behind one interface.
Architecture
- FrontendNext.js 16 and React 19 with Tailwind v4 and shadcn/ui. TanStack Query is the only client-side copy of server state; forms run on React Hook Form and Zod.
- APIExpress 5 in four modules, each shaped routes → controller → service → repository. An architecture test fails the build if a layer reaches past its neighbour.
- DatabasePostgreSQL through Prisma 6, with the constraints the app depends on written as raw SQL in the migrations. CI replays every migration and fails on drift.
- SharedOne package of Zod schemas, response types and formatters used by both apps — and the source of the generated OpenAPI document.
- HostingWeb on Vercel, API on Render. Next.js proxies the API path so session cookies stay first-party.
Technology
Interface
- Next.js
- React
- TypeScript
- Tailwind CSS
- shadcn/ui
- TanStack Query
- React Hook Form
- Zod
API & data
- Express
- Prisma
- PostgreSQL
- Argon2id
- JWT
- pino
AI
- Gemini
- Claude
Delivery
- Vercel
- Render
- npm workspaces
- OpenAPI
Engineering challenges
A timer the browser can't get wrong
Client clocks drift, sleep and can be changed. A reload, a sleeping laptop and a second tab would each tell a different story.
Exactly one running timer, even under a race
Two start requests arriving together would both pass an "is anything running?" check written in application code.
Days that aren't 24 hours long
Sessions cross midnight, daylight-saving days are 23 or 25 hours, and the viewer's day isn't the server's.
No one sees anyone else's data
One missing WHERE clause turns a task tracker into a data leak.
Solution
- S1
A timer the browser can't get wrong
The server owns the clock. Start and stop take no body — the server stamps both ends and computes the duration. The client anchors to the elapsed time the API reports and renders forward from it, refetching on focus, on reconnect and once a minute to catch a timer stopped on another device.
- S2
Exactly one running timer, even under a race
A partial unique index on each user's running log lets the database reject the second start — returned as a 409, its transaction rolled back. Stop is a conditional update, so two concurrent stops can't both succeed.
- S3
Days that aren't 24 hours long
A day is a half-open range in the viewer's IANA timezone, computed by PostgreSQL. Sessions are split across the days they touch, a running timer counts up to now, and a week is seven of those windows.
- S4
No one sees anyone else's data
The user id always comes from the verified session, every query is scoped by owner, someone else's task returns the same 404 as a missing one, and an authorization suite checks every task endpoint in both directions.
Performance
- Analytics are aggregated in SQL, so the browser receives a small JSON summary instead of every time log.
- Indexes are written for the queries the app actually runs.
Product experience
- Dark interface with an orange accent — a sidebar on desktop, a drawer on phones — and it respects reduced-motion settings.
- Starting a timer on a pending task moves it to in progress; completing a task stops its timer.
- The selected week lives in the URL, so a week of work can be shared as a link.
- The AI helper only fills in the form; you still review and save the task. With no provider configured, everything else keeps working.
