Engineering Recurring Reactive Invoices

Short answer: I thought recurring invoices were just cron + template. I was wrong. Again.

The Problem: Why Cron Doesn't Work Without a Server

I started with the obvious. Rust + tokio + `tokio::time::interval`. Check every 30 seconds, see if it's time. Works? Works while the app is open. Close it — nothing. No background processes, no daemons. Tauri isn't a browser with a service worker, it's a desktop window that dies with the process.

Okay, I thought, let's use the system scheduler. Windows Task Scheduler, macOS launchd, Linux cron. But that means: installer, admin rights, cross-platform hell. I promised users "download — run — works." Not "download — let the system change your schedule — reboot — pray."

Third option — remind the user to open the app. Push notification: "Time to send an invoice!" But push needs a server. And we don't have one. And a freelancer who hasn't opened the app in three weeks won't open it because of a reminder — they're on deadline, traveling, sick, just tired. I'm that person.

The constraint that changed everything: the app is the scheduler, but the app isn't always running. Any solution had to be idempotent, timezone-aware, and work after arbitrary gaps.

About Alexander

Alexander is a former compatriot. Lives in Portugal now, I never counted how many years. Fifteen-plus in the profession. When he talks about systems, he uses the word "invariant" more often than "function." Every sentence is a claim you can verify. No "probably," no "maybe."

We met on a social network. I posted about LockMargin, he replied: "How do you handle eventual consistency in offline mode?" I didn't know what to say. He wrote: "Message me when you do recurring invoices." I messaged him three months later.

He replied two days later. "I'm hiking, bad signal. But your problem isn't scheduling. Your problem is state reconciliation. Remember that word."

The Solution: State Reconciliation, or "Don't Ask What Time It Is — Ask What State the Invoice Is In"

I rewrote everything the next day. No cron. No timer. Reactive state check.

On every app launch (and every 30 seconds while open — our dashboard refresh cycle) we compute: "what should have been generated since the last check?" And create missing invoices in a single SQLite transaction.

The core query is deceptively simple:

SELECT * FROM recurring_invoices WHERE next_due <= :now AND (last_generated IS NULL OR last_generated < :period_start) AND deleted_at IS NULL;

But `:period_start` is where it gets hairy. An invoice on the 31st doesn't exist in February. "Last day of month" in one jurisdiction is the 28th, in another the 30th, in a third the accountant says "move to the first business day of next." We store schedules as a compact expression `(day_of_month | day_of_week, interval, timezone)` instead of full cron — because a freelancer in Toronto billing a client in Sydney needs the invoice dated in the client's business day, not the server's (which doesn't exist).

The Immutability Paradox: Snapshots vs. References

The second problem I didn't see coming: immutability. Once an invoice is "sent" — it's legally frozen. But recurring templates evolve: rates change, line items get added, tax gets adjusted. If the invoice stores a reference to the template — a template change on March 15 retroactively changes the March 1 invoice. That's not a bug. That's tax fraud.

The fix — snapshot billing: each generated invoice captures the template state at generation time, not a reference to it. This is correct for accounting, but surprised every beta tester who expected "update template > all future invoices update." We added a "propagate to future only" toggle. Alexander said: "You're teaching users to think like accountants. That's good."

Edge Cases That Bit Us

Deduplication: Force-Close Mid-Generation

Our biggest bug: user force-closes the app mid-generation — gets a duplicate invoice. Fix: `idempotency_key = SHA256(recurring_id || period_start)` with a `UNIQUE` constraint. Same invariant we use for manual invoice creation (lesson BR-32, learned early).

The Catch-Up Problem: Three Months Without Launching

If a user hasn't opened the app for three months — generate all three invoices at once? We cap at one and show a warning. Backdated bulk generation violates accounting principles in most Tier-1 jurisdictions (IRS, HMRC, CRA all require invoices dated when issued, not "when they should have been").

DST: Midnight That Doesn't Exist

"Midnight on the 1st" doesn't exist on DST transition days in some zones. We anchor all recurring schedules to UTC noon, then display in the user's local timezone — no DST gaps, no ambiguous times.

Audit Trail: Who Created This Invoice?

Every auto-generated invoice is tagged `created_by: 'system'` + `generation_source: 'recurring'`. Auditors and users themselves need to see the difference between manual and automatic. Alexander insisted: "If you can't explain to an auditor where this number came from, you've already lost."

Partial Failure: One Falls — The Rest Live

If generation 1 of 5 invoices fails (say, client soft-deleted), we log the error to `recurring_generation_log` and continue — don't block the rest. Alexander called this "graceful degradation." I called it "don't wake me at 3 AM because of someone else's soft-delete."

The Trade-Off We Took: No Email Sending "Out of the Box"

Email sending would violate offline-first — needs SMTP server, retry logic, bounce handling. Instead we generate an `.eml` draft + notification badge. The user decides when to send. Alexander said: "You're giving up control. That's right for a freelancer who doesn't trust automation with their money."

The Lesson: Three Questions for Any Offline Scheduling

Before you trust an app with your money:

How are gaps handled? If they say "just cron" — run. You need state reconciliation.

How are generations deduplicated? If no idempotency key — you risk duplicates.

What happens when the template changes? If "all invoices update" — that's tax risk. You need snapshots.

I'm not saying LockMargin calculates dates perfectly. I'm saying we think about edge cases most SaaS ignores — because they have a server that never sleeps. A freelancer's laptop shuts down. And that's okay.

FAQ

How does reactive scheduling work without background processes?

On every app launch and every 30 seconds while open, we compute which invoices should have been generated since the last check, and create them in a single SQLite transaction.

Why not use the system scheduler (cron, Task Scheduler)?

Requires installer, admin rights, and cross-platform support. Breaks the \"download — run — works\" promise.

What happens if I haven't opened the app for three months?

One invoice is generated — the oldest missed one. The rest generate one per subsequent launch. A warning is shown. Bulk backdated generation violates accounting principles.

How do you prevent duplicate invoices?

Idempotency key: SHA256(recurring_id || period_start) with a UNIQUE constraint in SQLite. Same period — same invoice, even on force-close.

Why no automatic email sending?

SMTP requires a server, retry logic, and error handling — violates offline-first. Instead we generate an .eml draft + notification badge.

P.S. — Alexander agreed to stay anonymous. He's real. We didn't grab coffee — he's in Portugal, I'm in Kharkiv, one hour ahead. He sent the audit at 2 AM his time. I read it at 4 AM because my cat fell off the shelf. Didn't reply immediately because I didn't understand half of it. Replied a day later, when I got to the DST section and realized I was calculating wrong. He wrote back: "At least you're honest." That was a compliment. I think.

Ready to own your freelance data?

Standard is $49 one-time — no subscriptions, no cloud, no data mining. Your data stays on your machine, encrypted with AES-256-GCM.

Download Free — No Account Needed Compare Pricing

What's Next

Post #6: How We Generate PDF Invoices That Don't Break on Windows 7 — or how we learned that PDF export isn't "just a library," but a whole engineering discipline.

Continue reading the Building LockMargin series.

Vlad (Volodymyr) Shiyan, founder of LockMargin

About the Author

Vlad (Volodymyr) Shiyan — Founder & Developer, Kharkiv, Ukraine. Building LockMargin since December 2025. Offline-first invoicing for freelancers who are tired of subscriptions. Standard is $49 one-time. Read more about Vlad →

Get one practical guide each month on building a business you own

No spam. No fluff. Unsubscribe anytime.

Back to top ↑