Building FollowUpInvoice: five bugs that failed silently
Why we built an invoice follow-up product, and five bugs from building it that never threw an error and nearly broke the whole point.
Our first product, FollowUpInvoice, does one job: it chases overdue invoices so you don't have to. It writes each follow-up, sends it from your own email address, and stops the moment the client pays.
This is the story of why we built it that way, and of the five bugs that taught us the most. None of them threw an error.
Why this product
Freelancers and small agencies don't struggle to send invoices. They struggle with the part after: the awkward email on day 7, the firmer one on day 14, the one they keep meaning to send. Money that is already earned sits in someone else's account while your own bills keep coming.
Plenty of tools say "AI invoice reminders". That is not a difference anyone can see. We decided the product had to be defined by three things the others don't promise together:
- You stay in control. Every draft can wait for your approval, and you can turn that off one step at a time.
- It sends from your own mailbox. Your client sees you, not a tool.
- The money never touches us. Card payments go straight to your own Stripe account through Stripe Connect. We are never the merchant.
It is deliberately not an invoice generator. It is the follow-up layer.
The five bugs
A follow-up product has an unusual failure mode. If it breaks, it breaks quietly: nothing sends, the app looks healthy, and the user finds out weeks later when the money still hasn't arrived. Every bug below was invisible in the normal way.
1. A fallback that looked like the real thing
When no AI provider key was configured, the app quietly fell back to static email templates. The emails looked AI-written. Nothing errored, and nothing told anyone.
The fix wasn't to patch the trigger. It was to make the fallback visible. Every email is now stored with how it was produced and shown in the dashboard as AI-written, Reviewed by you, or Template. If something falls back, you can see it.
2. A library upgrade that made every overdue reminder disappear
We upgraded to Carbon 3, where diffInDays() became signed. Our code checked whether the number of days overdue was in the list [1, 3, 7, 14, 30]. After the upgrade it was negative for overdue invoices, so the check never matched, and no overdue reminder was ever queued.
The fix: all overdue maths now goes through one helper that is clamped at zero, the dependency is pinned, and the behaviour has a test.
3. Exact-day matching loses work forever
Our first scheduler asked, "Is this invoice exactly 7 days overdue today?" If the cron job missed a day for any reason, the day-7 reminder was skipped forever, because tomorrow it would be day 8.
The fix is to be catch-up safe: each run finds the highest stage that is due and hasn't been sent, and deduplicates by type. A missed day now delays a reminder instead of deleting it.
4. A worker that quietly ignored half the work
We run two queues: one for transactional mail and one for reminders, so bulk chasing never delays an invoice email. A queue worker started without naming both queues drains only the default one. Reminders piled up unseen.
This was wrong in every deployment note we had written, all at once. The fix was a health endpoint that returns an error when the worker or the scheduler has stopped (the framework's default "up" check only proves the web server answered), with heartbeats for the scheduler and for each queue.
5. Approval that didn't approve what you read
When you approve a draft from Slack, you expect the client to receive that text. Our first version regenerated the AI copy at approval time, so the client could get different wording from what you had just read. That makes approval meaningless.
Now the draft is stored when it is created and sent verbatim when approved.
The pattern
Four of the five share a shape: the failure was silent, and the fix was to make the state visible: a provenance label, a clamped helper, a catch-up rule, a health check, a stored draft.
If you are building anything that runs in the background and reports success by saying nothing, ask one question early: how will I know it has stopped?
What we'd tell a client
These are exactly the problems we plan for when we build products for other people. The visible features are the easy half. The other half is the things that must not fail quietly.
See what we are building.
Our products →