The product features get the attention, but what actually keeps a SaaS alive is the operational spine — the invisible automation that onboards users, meters usage, retries failed work, and never drops a job. Here’s how to build it so it runs itself.
Event-driven, not cron-driven
The instinct is to poll: a cron that scans the database every few minutes looking for work. It doesn’t scale and it’s laggy. The better model is events: something happens (a signup, a payment, an upload), you emit an event, and a handler reacts. Cron still has a place — daily rollups, reconciliation — but the operational spine should be event-first.
await emit('user.signed_up', { userId, plan });
// handlers react independently: send welcome, provision resources, start trial clock
Queues absorb the slow and the flaky
Any work that is slow (a third-party API), spiky (a burst of signups), or failure-prone (email, payments) goes on a queue. The request returns instantly; a consumer drains the queue with automatic retries and a dead-letter queue for anything that keeps failing. This one pattern removes the majority of “the app hung” and “we lost that job” incidents.
Idempotency is non-negotiable
Retries mean every handler runs more than once. If “send welcome email” or “grant credits” isn’t idempotent, retries double-charge, double-send, and double-provision. Make each side effect safe to repeat — a unique key, an INSERT OR IGNORE, an “already done?” check before acting:
const claimed = await db.prepare(
'INSERT OR IGNORE INTO processed_events (id) VALUES (?)'
).bind(eventId).run();
if (claimed.meta.changes === 0) return; // already handled — stop
await grantCredits(userId);
Onboarding automation: the highest-ROI flow
Automate the path from signup to first value: provision the account, seed sample data, send a sequenced welcome, and nudge at the exact points users stall. Every manual step here is a step that breaks at 2am and a place users churn. The goal is that a new customer reaches their “aha” without a human touching anything.
Usage metering that bills correctly
If you charge by usage, meter it as events, aggregate on a schedule, and reconcile against the billing provider. Never trust a single counter you increment live — it drifts. Emit a usage event, sum them, and make the billing job idempotent so a re-run can’t double-bill. Metering bugs are trust bugs; treat them like money bugs.
Make it observable or it will lie to you
Log every job’s outcome, count retries, and alert when the dead-letter queue grows. An automation you can’t see is one you’ll discover is broken only when a customer complains. A five-line “how many jobs failed today” check catches more than a dashboard nobody opens.
The spine in one sentence: emit events, queue the heavy work, make every handler idempotent, and watch the failures. Get that right and the business runs overnight — which is the entire point of software. Frabo’s operator is built on exactly this shape on Cloudflare Queues and Durable Objects; the boring parts are what let the smart parts run unattended.
