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.

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 onlyundefined, 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.




