A daily chronicle of hours spent — personal time tracking for you and your team, rendered as a printed ledger.
Live: https://ttm.danielmoya.cv
TimeTracker is a small full-stack app for logging how you spend your day. It keeps a per-user ledger of sessions and breaks, groups them by customer and project, and renders everything as a chronograph dial + a paper ledger in an editorial "Almanac" aesthetic. Multiple members can share a workspace and keep independent ledgers.
- Chronograph-style timer with a sweeping second hand, monospace
HH:MM:SSdisplay, and a live note sublabel. - Start / stop a work session, attributed to an optional client → project (cascading picker).
- Inline slide toggle under the start button reveals a grid of quick-break tiles (Coffee · Lunch · Gym · Commute · Walk · Call · Partner · Kids · Errand · Rest). Tapping a tile while a work session is running cuts and switches in one action.
- Break quick-types with notes and icons — pick from built-in tiles or save your own with a label + icon from a 17-glyph palette.
- Day ledger lists today's sessions as numbered ledger rows with per-row delete (with an "ink strike" AlertDialog).
- Calendar view with dotted markers on days that have entries.
- Workspaces with role-based membership (owner / admin / member) and token-based invitations.
- Customers + projects are shared catalogues across workspace members; projects may optionally tie to a customer.
- Time entries and personal templates are scoped to
(workspace, user)— each member has an independent ledger, analytics, and invoices.
- Dashboard — today stats, week rollup, active timer, top customers, recent activity.
- Patterns — hourly + weekday productivity, break frequency.
- Insights — narrative summaries + recommendations.
- Custom reports with
day / week / monthgrouping. - This-week vs last-week period comparison with trend arrows.
- Per-customer weekly goal progress rings.
- Full-text search across entries with date-range, customer, project, and duration filters (paginated).
- Monthly PDF invoice generation (PDFKit), rendered in the same editorial style as the web UI — masthead, ruled table, vermilion section labels, monospace duration column, italic colophon.
- JWT auth with a 7-day expiry; bcrypt password hashing (cost 10).
- Client-side token-expiry check + 401 interceptor that signs the user out on the first stale response.
- Sign-out lives in the workspace menu (desktop) and the Members card (mobile).
express-rate-limiton/api/auth/*and/api/generate-invoice.- Zod schemas on every write endpoint.
- WebSocket (
wspath) pushes dashboard events to open clients. - PWA manifest + full icon set (favicon 32/192/512, apple-touch, maskable) — installs to the home screen.
- Mobile-first layout with a thumb-reachable bottom tab bar + an editorial masthead / tab rail from
sm:up. - Typography: Fraunces display serif, Newsreader body serif, JetBrains Mono for every numeric readout.
- Parchment palette with a single vermilion accent, hairline rules, and a very faint SVG-grain overlay.
prefers-reduced-motionrespected throughout.
┌──────────── internet ────────────┐
│ 443
▼
Nginx Proxy Manager
(TLS · HTTP/2 · HSTS)
│ http
▼
ttm (Node/Express) ─── client: React + Vite + Tailwind
│ postgres wire
▼
ttm-db (HeliosDB-Nano) ─── persistent volume
Two containers on a private Docker bridge network, fronted by Nginx Proxy Manager with a Let's Encrypt certificate. The app is served over HTTPS, proxies /api/* and the /ws WebSocket path to the Node backend, and talks to HeliosDB-Nano over the standard PostgreSQL wire protocol.
| Area | Choice |
|---|---|
| Frontend | React 18 · TypeScript · Vite · Tailwind · shadcn/ui (Radix) · @tanstack/react-query |
| Charts | Recharts |
| PDFKit | |
| Fonts | Fraunces · Newsreader · JetBrains Mono |
| Backend | Express 4 · TypeScript · Zod · express-rate-limit · jsonwebtoken · bcryptjs · ws |
| ORM | drizzle-orm/postgres-js |
| Driver | postgres (postgres-js) |
| Database | HeliosDB-Nano |
| Reverse proxy | Nginx Proxy Manager |
| Platform | Docker Compose on a single VPS |
Nine tables across six migrations — see drizzle/:
| Table | Scope |
|---|---|
users |
global |
workspaces, memberships, invitations |
per-workspace (with role) |
customers, projects |
shared across workspace members |
time_entries, entry_templates, invoices |
scoped to (workspace, user) |
TimeTracker uses HeliosDB-Nano as its operational database. HeliosDB-Nano is a single-binary embedded database that speaks the PostgreSQL wire protocol (plus MySQL and HTTP REST), storing data in a RocksDB-style layout at rest. It has persistence, SERIAL/IDENTITY columns, FK constraints, pg_catalog views, extract(epoch from ...), window functions, and the rest of the PG surface a typical ORM expects.
The app connects via a standard postgres-js + drizzle-orm/postgres-js setup — no custom driver, no adapter shim, no HeliosDB-specific code paths in the application. The db/connection.ts file is six lines long.
This project doubled as an integration test of HeliosDB-Nano against an unmodified Drizzle-ORM stack. We deployed an everyday React + Drizzle + postgres-js codebase, gave it no HeliosDB-aware workarounds, and filed bugs on everything the server got wrong vs stock PostgreSQL 16. The Nano team closed 36 compatibility bugs across nine releases (3.14.0 → 3.14.10):
| Area | Releases |
|---|---|
SERIAL / IDENTITY auto-increment, DEFAULT in VALUES, RETURNING * wire format |
3.14.0 / 3.14.3 / 3.14.4 |
Extended-query protocol (Parse/Bind/Execute/Flush), pg_catalog.pg_type introspection, catalog WHERE filters |
3.14.1 / 3.14.2 |
NOT NULL enforcement parity across INSERT paths; DEFAULT now() evaluation; INSERT … DEFAULT VALUES |
3.14.3 |
extract(epoch from ts-ts), timestamp wire encoding, date/time comparison, ISO-string → timestamp coercion |
3.14.2 / 3.14.5 / 3.14.7 |
Stale result_cache after INSERT RETURNING (the sneaky one: parallel reads returning [] after a fresh write) |
3.14.6 |
UPDATE/DELETE with a table-qualified WHERE column; parameterized LIMIT/OFFSET; UPDATE SET coercion |
3.14.7 / 3.14.8 |
| Mixed qualifier styles across SELECT list vs GROUP BY; DATE group-key comparison | 3.14.9 |
| FK validation with quoted target identifiers (closed the last orphan-row data-integrity hole) | 3.14.10 |
End state: TimeTracker runs on HeliosDB-Nano 3.14.10 with idiomatic Drizzle — no sql.unsafe() escape hatches, no manual SQL, no ORM-level workarounds. The full bug list with verbatim reproducers and resolution notes lives upstream in the HeliosDB-Nano repo as BUGS_TIMETRACKER_DRIZZLE_COMPAT.md.
If you want to run your own Drizzle (or Prisma / TypeORM / SQLAlchemy / etc.) app on Nano today, you inherit all of the above — every fix landed on main.
# Clone, install, start Vite dev server + API
npm install
npm run devThe dev server proxies /api/* to http://0.0.0.0:3001 (see vite.config.ts). The backend needs a running HeliosDB-Nano (or any Postgres) on DATABASE_URL.
Environment variables for the application container:
| Env | Purpose |
|---|---|
DATABASE_URL |
postgres://postgres@ttm-db:5432/heliosdb |
DATABASE_SSL |
false on a private Docker network |
JWT_SECRET |
32-byte random, base64 |
NODE_ENV |
production |
PORT |
3001 |
Build and deploy both containers (application + HeliosDB-Nano) on the same Docker network, fronted by whichever reverse proxy you like. The full operational manual lives in SERVICE.md — component inventory, migration runbook, rollback matrix, secrets inventory, backup plan.
.
├── client/ React + Vite frontend (editorial "Almanac" theme)
│ ├── index.html Vite entry, favicon + manifest links
│ ├── public/ Favicon set + web manifest (served verbatim)
│ └── src/ Components, pages, hooks, lib
├── server/ Express + Drizzle backend
│ ├── routes.ts Core CRUD (users, customers, projects, time entries, invoice)
│ ├── auth.ts Register / login / JWT middleware
│ ├── workspaces.ts Workspace + membership + invitation routes
│ ├── projects.ts Project CRUD (FK to customers)
│ ├── templates.ts Entry-template CRUD (break quick-types with icons)
│ ├── bulk-operations.ts Bulk update / delete / export / statistics
│ ├── advanced-features.ts Search, dashboard, patterns, reports
│ ├── productivity-insights.ts
│ ├── notifications.ts
│ ├── websocket.ts /ws real-time event push
│ ├── validation.ts Zod schemas for every POST/PATCH
│ ├── rate-limit.ts express-rate-limit config
│ └── index.ts Server bootstrap
├── db/
│ ├── schema.ts Drizzle schema (canonical source of truth)
│ ├── connection.ts postgres-js client (six lines)
│ └── migrate.ts Migration runner
├── drizzle/ SQL migrations (0000 → 0005)
├── Dockerfile Multi-stage build (Vite + esbuild → Node 20 Alpine)
├── SERVICE.md Operations / deployment manual
└── README.md
MIT — do what you like. Attribution appreciated but not required.