← back to all work

Revenue data layer & monitoring

One set of numbers for the whole funnel, from paid lead to cash collected, and a layer that watches the systems themselves. Because a scheduled job that reports "succeeded" can still be doing nothing.

RoleArchitect and lead engineer; author of the leadership reporting
TimelineFeb 2025 to present
StatusIn production
ContextBuilt at Oneday, an online education company selling degree programmes to US founders. One funnel, seven stages, five teams. I own the revenue numbers for the business and write the software that runs it. Reporting was rebuilt first, then every system above was built to report into it, then the monitors were built to watch all of it.
Postgres + SQLSupabase edge functionspg_cronBitrix24 CRMAirtableBrevo → ActiveCampaignPower BIGoogle SheetsDiscord + email digests
97 / 78live functions / scheduled jobs
203,521automated emails to 50,829 people · 162 templates
5,755findings from 25 monitors · 286 high severity
4.1%spend forecast error at day 25 · 30-month backtest

01 — The problemThree tools, three answers to "how many did we sell".

The CRM, the marketing platform and a spreadsheet each held a version of the funnel. Lead-to-won was computed differently in each, cohorts mixed partial months with complete ones, and cost per lead stood in for cost per customer. Leadership decisions were being made on numbers nobody could reconcile.

Underneath, dozens of automations ran on schedules and nobody knew when one stopped. The scheduler said every run succeeded. It meant the request was accepted, not that the work was done.

02 — What I builtA data layer every system reports into, and alarms on the layer.

DataTwo-way CRM syncThe CRM, the operations database and the marketing platform are kept in sync both ways, with forward-only cursors and explicit conflict rules. 71,208 contacts were migrated between marketing platforms with 85,592 verified.
DataCohort reportingLead-to-enrolment measured one way, by apply-month cohort, in one timezone, with partial months marked. Cost per customer by channel, rebuilt from lead to cash. Spend forecasts within 4.1% at day 25.
DataLeadership memosA weekly written readout: what moved, why, and the decision it implies. Who we actually close versus who we think we sell to. Why people miss calls. Where to spend next.
WatchFunnel detectors25 detectors run on a schedule against the data layer: dropped inbound messages, night-time sends, dead webhooks, missing confirmation emails, stalled queues, failed syncs. Findings are fingerprinted so one problem raises one alert.
WatchReal job outcomesScheduled HTTP calls are collected with their actual response status and joined to the tick, so a function that fails after the accept window shows as failed, not succeeded.
WatchDaily digestOpen findings go to the owner each morning by email and chat, with a diagnosis drafted by an LLM from the finding's own evidence.

03 — How it worksWhat a detector looks like, in practice.

-- inbound texts with no routing decision in the last 24h
select c.thread_id, c.timestamp
from conversations c
where c.direction = 'Inbound'
  and c.timestamp > now() - interval '24 hours'
  and not exists (
    select 1 from workflow_log w
    where w.phone = c.thread_id and w.timestamp >= c.timestamp
  );
  1. Each detector is a saved query with a severity, an owner and a fingerprint rule. Most were written the day an incident was found and encode exactly that failure.
  2. Findings are upserted by fingerprint, so a problem that persists is one finding with a count, not a hundred alerts.
  3. High-severity findings page the owner; the rest wait for the morning digest.
  4. A finding is closed by the data, not by hand: when the query stops returning rows, it resolves.

04 — What brokeIncluding the one where the alarm worked and nobody moved.

Success that was notA scheduled call has a five-second accept window. A function that ran 120 seconds and then failed reported success on every tick for five days.
Collect the real responseReceipts are joined to the actual HTTP outcome. "Succeeded" now means the function returned 2xx.
Deployed but never scheduledA function was deployed and never ran, because no schedule row existed. It failed silently for weeks.
Deploy checklistAfter deploying a scheduled function, the schedule table is checked and the function is fired once by hand. The deploy script enforces the auth setting the schedule needs.
A redeploy lost every robot emailA redeploy flipped the auth mode on the email sender. Every CRM-triggered email returned 401 while service calls kept working. Nothing looked broken. It happened twice.
Guard the settingThe deploy script reads the live auth mode and hard-fails on a mismatch. An email-coverage detector now watches sends against stage moves.
The alarm nobody acted onA calendar event was renamed. A hand-copied title filter stopped matching, and about 250 advisory calls got no confirmation or reminder for a week. The coverage detector fired daily from day one. Nobody acted on it.
Shared gates, and findings as outagesTitle filters import one shared pattern and match both names of a renamed event. An unactioned high finding is now treated as an outage, with escalation, not as a notification.
216 texts, 32 night slotsThe first two detectors to run found 216 silently dropped inbound texts and 32 night-time slot offers that had already gone out.
Fix, then keep the queryBoth were fixed in the agents the same week. Both queries still run every day.

05 — What I'd do differentlyBuild the monitoring before the second system, not after the sixth.

Every detector exists because something broke first. That is the honest order most teams build in, and it cost real leads. The second thing I would change is ownership: a finding without a named owner and an escalation path is a log line. The advisory outage proved that an alarm is not the same as a response. Every detector now carries an owner, and I would make that a condition of shipping one.