Skip to content
← All builds
MB-302SoftwareIn daily use · SaaS in development2026

TymTracker

A time-tracking and invoicing app I built when my invoicing tool's price went from about $100 to about $2,000 a year. It runs my own invoicing today, and is being rebuilt as a multi-tenant SaaS product.

TymTracker dashboard with unbilled hours, outstanding and collected totals, open invoices and this week's hours, using made-up demo data.
Fig. 1 · TymTracker dashboard with unbilled hours, outstanding and collected totals, open invoices and this week's hours, using made-up demo data.

It covers the whole loop: a weekly timesheet, projects and customers across several businesses, a four-step invoice wizard that only pulls uninvoiced time and billable expenses, PDF invoices by email, and automatic past-due notices. Invoiced time locks so the books can’t drift.

Decisions

  • Money as integer cents, converted in exactly one place. Every money column was renamed (hourlyRateCents, totalCents) so the compiler found every use. The API rejects non-integers and the server recomputes invoice totals.
  • A migration that reports instead of silently fixing. The move from SQLite to Postgres runs in one transaction as a dry run by default, and reports row counts, money totals, rounded sub-cent values and any invoice whose total doesn’t match its lines.
  • Evolve in place. My own data migrates in as the first tenant. The live single-user instance keeps running until cutover.
  • Plan limits are data, not code, so pricing can change without a deploy.

What the agent audit found

  • Forgeable sessions. The live instance was signing sessions with a placeholder secret from the compose file. Fixed, and the app now refuses to start with a missing, short or placeholder secret.
  • Public uploads with stored XSS. Uploaded files were reachable without login, and file type came from the client’s filename. Fixed with magic-byte checks and by putting uploads behind auth, then verified from outside the network.
  • The Save button that did nothing, three times. The database returns null, the form schema accepted only undefined, and validation failed silently. The rule is now written into the project’s agent memory so it can’t come back.

Screenshots are from a local copy running on made-up seed data, never the live instance.

Weekly timesheet where invoiced cells show a padlock.
Fig. 2The weekly timesheet. Cells already on an invoice lock, so billed time can't drift. Demo data.
Step 3 of the invoice wizard, listing line items built from uninvoiced time.
Fig. 3Step 3 of the invoice wizard: line items built only from uninvoiced time.
The top of a generated PDF invoice.
Fig. 4The generated PDF invoice (React-PDF).