Harbour & Finch
- Subtotal
- 5,160.00
- Tax
- 0.00
- Total due (CAD)
- CA$5,160.00
Time tracking and invoicing for freelancers, built from scratch, and what it taught me about making software people can rely on.

I started TapBill with a challenge for myself. Could I build a complex, production-quality application on my own, using AI to speed up development, that could genuinely replace a paid service like Harvest or Toggl for some freelancers? Not another impressive-looking prototype. Something people could use to manage projects, track their work, and send real invoices.
Over about four months of evenings and weekends, I went from an idea to a working product with real users. AI made it possible to move quickly, but building TapBill taught me something I keep coming back to.
“Writing code is only one part of creating good software.”
I’ve become fascinated by products that feel effortless to use. Linear is one of my favourite examples. There’s nothing new about a list of issues, but its shortcuts, layouts and small interactions make the experience incredibly fast and intuitive. I wanted to bring that same attention to detail to TapBill.
How quickly can someone create a project and start tracking time? Do they understand what to do next? Can they create an invoice without worrying about how the finished document will look?
The invoice builder went through roughly seven iterations as I gathered feedback from early users. One of my favourite improvements is a side-by-side editor that updates the finished invoice as you type. Users can see exactly what they’re creating, make changes, add their signature, and save a draft before publishing. It’s a relatively small interaction, but it makes the experience clearer and gives people confidence in what they’re about to send. Try it here: change a line, add one, then finalize it.
Harbour & Finch
The same thinking is behind the ticket every new member signs before they start. You can sign one yourself in the lab.
I’ve also learned when not to build something. AI makes it incredibly easy to add another feature or animation. The harder skill is recognizing when it doesn’t improve the experience.
One of the biggest changes in how I think about software has been my understanding of architecture. I used to think of architecture mostly as how code was organized. Building TapBill taught me that some of the most important decisions happen much earlier, in the data model.
Take invoices. At first, invoice numbers were assigned when a draft was created, so abandoned drafts left gaps in the numbering sequence. Eventually, I moved numbering to the moment an invoice is finalized. That decision also clarified something important: an unnumbered draft can be deleted, but an issued invoice needs a documented history. Invoices now follow a defined lifecycle, with rules around issuing, sending, payment, cancellation and replacement.
I also learned that important business rules shouldn’t live in just one form or server action. PostgreSQL constraints and triggers enforce rules around invoice numbering, paid invoice entries and billable time. Even if another part of the application attempts an invalid change, the database is the final safeguard.
Here’s one of them. Once money has been received against an invoice, the hours on it can never be handed back to the unbilled pool, so already-paid work can’t be billed twice. The app already refuses this, and the trigger makes sure every other path does too, including imports and maintenance scripts.
-- Hours that money has been received against are never released.
CREATE OR REPLACE FUNCTION enforce_paid_invoice_entry_lock()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (
SELECT 1
FROM invoices i
WHERE i.id = OLD.invoice_id
AND i.paid_at IS NOT NULL
) THEN
RAISE EXCEPTION
'time entry % cannot be released from invoice %: it has a recorded payment',
OLD.id, OLD.invoice_id
USING ERRCODE = 'integrity_constraint_violation';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER time_entries_paid_invoice_lock
BEFORE UPDATE OF invoice_id
ON time_entries
FOR EACH ROW
WHEN (OLD.invoice_id IS NOT NULL AND NEW.invoice_id IS DISTINCT FROM OLD.invoice_id)
EXECUTE FUNCTION enforce_paid_invoice_entry_lock();It checks paid_at, not the status. Cancelling an invoice rewrites its status before it touches the hours, so a status check would look at the wrong value and let the change through. paid_at is the record that money actually moved.
Time tracking brought another interesting challenge. A timestamp represents a specific moment, but an invoice date represents a calendar day. Treating them the same way led to timezone problems when moving from local development to production. Fixing that meant being deliberate about data types, timezones, and how dates move between PostgreSQL and JavaScript. These aren’t features users notice, but getting them right prevents entire categories of problems.
TapBill also gave me the chance to build a reusable component system. The application uses custom React components built with Radix primitives, Tailwind CSS and Framer Motion.
As the interface grew, I found myself solving the same problems in different places. Buttons needed consistent behaviour, forms needed predictable layouts, and tables needed to support different kinds of data without becoming completely different implementations. I started consolidating those decisions into shared components. Today, the library includes form controls, dialogs, tables, date pickers and other interface elements.
Some of my favourite details came from fixing real problems. Icon-only buttons require an accessible label through their TypeScript interface, so a button with no name won’t compile. Buttons keep their width when they switch to a loading state, avoiding the small layout jumps that make interfaces feel less polished. And the shared table supports different datasets, selection, actions and empty states without rebuilding the interface for every feature.

Owning the component library means maintaining it, dealing with edge cases, and sometimes realizing that an abstraction I thought was clever needs to be simpler. It’s taught me a lot about composition, consistency, and designing components that are easy to use and maintain.
I’ve used AI agents throughout TapBill’s development, but I’ve become much more deliberate about how I work with them. Earlier in the project, I experimented with extensive development documentation and instruction files. Over time, I learned that more context wasn’t always better context. I moved active development work into Linear, cut unnecessary documentation, and began defining clearer tasks with focused requirements.
Before releases, I use an agent-assisted review to check code against both the development standards and the original task. But I don’t consider an agent’s approval proof that something works.
So I built a verification process. It runs against a clean copy of the exact commit, creates a throwaway PostgreSQL database, applies migrations, builds the application, and runs end-to-end tests with Playwright.
npm run verifystep "Clean copy of ${sha:0:9}"
step "Throwaway Postgres 18 on port $port"
step "Install"
step "Migrate"
step "Unit tests"
step "Postgres rule checks"
step "Production build"
step "Types"
step "Browsers"
step "E2E"The deployment script checks that the exact commit being shipped has already passed verification.
On the reviewed version of the project, that process completed 159 tests successfully, with one skipped. It isn’t a perfect system, and I’m still improving it. But it’s helped me move from asking whether code looks correct to having a repeatable way of checking whether it actually works.
TapBill stores information that freelancers and businesses may need long after an invoice is sent. That creates responsibilities beyond building a good interface.
Users can export their time and invoice records. They can also request account deletion, with a 30-day recovery window in case they change their minds. Behind that simple interaction is a process for ending sessions, removing access, managing connected services, and eventually purging account data.
Working through these decisions has made me think more carefully about security, data ownership, and what happens when something goes wrong. It’s also shown me how much responsibility comes with building software for real people.
TapBill started as an experiment to see how much I could build with AI. Four months later, I had a working product, real people using it, and a much better understanding of what it takes to create reliable software. I’ve learned to think more carefully about data modelling, system design, component architecture, testing, and the decisions that affect users.
“Building more isn’t necessarily building better.”
Sometimes the right decision is a new feature. Sometimes it’s a better abstraction, a database constraint, or removing something that never needed to exist. AI has changed what’s possible for me as a developer, but understanding the problem and taking responsibility for the result are still the parts I care about most.
And I’m proud of what I’ve built.
TapBill is open to invited users, and I’m looking for freelancers and small businesses interested in trying it. If you track time, manage client projects, or send invoices, I’d love to hear what you think.
East Fraser Lands
Project and community updates for River District, a waterfront neighbourhood on the Fraser River.
Read the next one →