Skip to content
← All builds
MB-302SoftwareIn daily use · SaaS on staging · iOS beta2026

TymTracker

A time-tracking and invoicing product I built when my invoicing tool's price went from about $100 to about $2,000 a year. A multi-tenant web platform plus an iOS and Android app for invoices, estimates and clients on the go.

The TymTracker iPhone app: home with this week's hours and money owed, the open invoices list, and the weekly timesheet. Client names and amounts are illustrative.
Fig. 1 · The TymTracker iPhone app: home with this week's hours and money owed, the open invoices list, and the weekly timesheet. Client names and amounts are illustrative.

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.

The phone app

A companion iOS and Android app built with Expo and React Native on the same API:

  • Sign in with email (with two-step verification), Google or Apple, then home, invoices, projects, expenses, clients and reports.
  • Invoice from the phone: pick a client with unbilled work, make the draft, send it.
  • Estimates: follow up, share the accept link, and turn accepted ones into invoices.
  • Push notifications when an invoice is opened, an estimate is accepted or a payment lands; tapping one opens it.
  • Offline: the last-loaded data stays available without a connection.
  • Workspace settings, members and history on the phone too. It’s in TestFlight on iOS.

Getting paid

  • Online card payments through Stripe Connect. Each business connects its own Stripe account, so the money never passes through mine.
  • Opt-in card fees that follow US surcharging rules: credit cards only (never debit or prepaid), at most 3%, blocked in the states that ban them, and shown before the client pays.
  • Late fees, recurring invoices, prepaid-hour retainers and a statement link per client.
  • Exports to QuickBooks and Xero, a profitability report, and a self-serve import from Harvest.

Built for AI assistants

Each workspace can issue API keys, and an MCP server lets an AI assistant log time, look up unbilled work and draft invoices on the user’s behalf, with the same permissions as the key.

Ready for more than one tenant

  • Every calendar day belongs to a workspace. Each workspace has its own time zone and week start, and the hourly jobs work out “today” per workspace. The server runs in UTC, so nothing derives a date from the server clock.
  • Race-free plan limits. The check and the insert run in one transaction under a Postgres advisory lock, so two people can’t both take the last seat.
  • Rate limits and a CSRF origin check in front of every route, with the limiter’s counters in Postgres rather than in memory.
  • An audit log on invoices, estimates and settings, on the web and in the app. Entries name the fields that changed, never the values.
  • Production plumbing: uploads go to private object storage behind five-minute signed links, and error reports strip personal data before they leave.

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.

Client names, projects and amounts in the phone screenshots have been changed.

The TymTracker iPhone app's timer and account screens.
Fig. 2The phone timer, and the account screen with projects, clients, expenses, estimates and notifications.
The TymTracker web dashboard with made-up demo data.
Fig. 3The web dashboard, on the same account and API as the phone app.
Weekly timesheet where invoiced cells show a padlock.
Fig. 4The 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. 5Step 3 of the invoice wizard: line items built only from uninvoiced time.
The top of a generated PDF invoice.
Fig. 6The generated PDF invoice (React-PDF).