Case study · Data platform
Morning Metrics
Eight platforms, eight versions of the truth. One pipeline that ends the argument.
Solo build. A data pipeline, a multi-tenant web app and an MCP server. Researched, designed, built and shipped by me.
- ELT pipeline
- Data warehouse
- Multi-tenant app
- MCP server
- OAuth 2.1
Panel 1 of 3. Before the first meeting
Eight tabs, eight versions of the truth
It is 7:40am. Coffee, and eight tabs.
Google Ads, Meta, Microsoft, GA4, Search Console, Merchant Centre, the email platform, the store’s own admin. Always in the same order, because I have been doing it long enough to have an order.
Every one of them is telling me a story, and every story is true. Add the platforms up and they have collectively sold more than the business actually took.
None of them are lying. They are each answering a slightly different question: a click they touched, a window they chose, a conversion they counted their own way. Alone, every number is defensible. Together they do not reconcile, so people stop trusting all of them and argue from instinct instead.
That was the moment. Not a gap in a market. A spreadsheet I rebuilt every morning to answer a question eight systems already had the data for.
Screen to come
Four clocks, four times, and not one of them wrong
Three things that bothered me every morning
None of it was hard. It was wrong in the same three ways every day, which is worse.
Nobody could agree on a number. Not out of malice, out of definitions. Revenue meant six different things depending on the tab, and nobody had written down which one we ran the business on.
Every question was a project. Which search terms burned money last month? meant an export, a pivot table and half a morning. So mostly, nobody asked.
The answer always arrived after the decision. Reporting was archaeology. By the time the picture assembled, the week it described was over.
Land everything, define it once, serve it everywhere
Morning Metrics is one idea, repeated.
Every source lands raw in a warehouse overnight. A transform step turns that history into a small set of daily summary tables: spend, revenue, orders, keywords, products, organic queries, email flows. The definitions live in code rather than in someone’s head, and those summaries are the only numbers anything is allowed to read.
Three things read them. The app, which is dashboards, campaigns, products, attribution, and a nightly rules engine that turns wasted spend and silent email flows into a dollar-valued to-do list. The brief that lands in your inbox at 7am. And an MCP server that lets a marketer connect Claude to the workspace and just ask.
That last one matters more than it sounds. Ask the chatbot what our MER was last week and you get the figure the dashboard shows, because there is exactly one place that figure lives. No more “the dashboard says X but the report says Y”.
The whole thesis is one loop, run every night.
Land
Every source arrives raw and is kept exactly as the platform sent it, wrapped in a versioned envelope.
Land → Define → Serve → Act → Land again
- Land. Every source arrives raw and is kept exactly as the platform sent it, wrapped in a versioned envelope.
- Define. A transform step turns that raw history into daily summary tables, with every definition written down in code.
- Serve. The app, the 7am brief and the MCP server all read those summaries. Nothing is allowed to read anything else.
- Act. The rules engine prices the waste, the brief lands before the first meeting, and tonight it runs again.
Building it
I built it solo, with AI coding assistants doing the typing while I did the deciding. That is not a footnote. It is why a marketer shipped a data platform instead of a deck about one.
It is ELT rather than ETL, deliberately. Land the raw history first and transform later, so a definition I get wrong in month two can be recomputed in month three without re-asking eight APIs for data they no longer hold. The warehouse keeps everything, cheaply. The app database keeps only the summaries a page load can afford.
But the interesting problem was never the dashboard. It was making a dozen inconsistent feeds behave like one dataset.
Contracts, not conventions. Every record arrives in a versioned envelope, against an explicitly named dataset rather than whatever shape the API felt like returning that day. Money stays as strings and goes through decimal arithmetic, because floats quietly lose cents at scale. Ratios are recomputed from totals, never averaged. Averaging an average is the most common way a dashboard tells a confident lie.
Capabilities, so nothing guesses. Each connector declares what it can actually provide. Ask a source for keyword data it does not have and the request is rejected before the adapter runs, rather than returning an empty result that reads on screen as a zero. A wrong number is worse than a missing one.
Idempotency, so nights are repeatable. Loads stage into a temporary table, then transactionally replace only that source’s slice of that date window. Re-running a failed night is a no-op, not a duplicate. Every pipeline I had ever been handed as a marketer failed that test, usually on the day I needed it.
Isolation and secrets. Every query is filtered by workspace membership, so one brand’s data cannot surface in another’s. Refresh tokens are encrypted at rest with key versioning, and a source credential can never be used by a destination.
The AI layer, treated as untrusted. The MCP server runs on OAuth 2.1 with PKCE, a consent screen, revocable grants re-checked on every request, rate limits that fail closed, and an audit event per query. Ad copy from the platforms is handed to the model labelled as data, not instruction, because the one thing an attacker can write into your pipeline is ad text.
The envelope every record arrives in
The unglamorous half of the build, and the part I would most want to be asked about. Eight fields wrapped around every row, so any number the app shows can be traced back to the account, the day and the sync that produced it. Two of them earn their place on their own. Currency, because adding AUD to USD is the quietest available way to be wrong by a third. Synced at, because platforms restate the last few days and you want to be able to see which read won.
| Field | Carries | Guards against |
|---|---|---|
| Provider | The platform the row came from | Two sources claiming one campaign |
| Dataset | An explicitly named shape | A connector inventing its own |
| Schema version | The version of that shape | An API change corrupting history |
| Source account | The ad or property account | Spend with no owner |
| Date | The day the metrics belong to | A re-run duplicating a night |
| Currency | What the money is denominated in | Adding AUD to USD |
| Timezone | The account’s reporting timezone | Disagreeing about yesterday |
| Synced at | When the row was fetched | A restatement overwriting a newer read |
The record envelope as implemented in the Morning Metrics ingestion layer. This is the design rather than a measurement. Nothing on this page was read off a report, because nothing on this page is a result.
What is missing from this page
There is not a single result on this page, and you should notice that.
Morning Metrics exists to make numbers trustworthy, so it would be a strange place to put an untrustworthy one. I have no user count worth quoting, no measured hours saved, and no before-and-after on a decision that got made faster because the brief arrived at 7am. Each of those would take a minute to write and would not survive being asked about.
The version of this page I would rather be showing you is the one with a baseline on it. I never timed the eight-tab morning before I replaced it, so the saving is obvious to me and unprovable to you. Measuring the thing you are about to change is the cheapest work in any project and the easiest to skip, and I skipped it.
What is here instead is the architecture and the reasoning under it. If you want to test a claim on this page, test a design call: why ELT, why money is a string, why the MCP server treats ad copy as hostile.
What I took from it
I started this as a marketer with eight tabs and a spreadsheet. I finished it having designed a schema, written a connector layer, and made real calls about idempotency, tenancy, credential handling and prompt injection.
The most useful thing I learned was not technical. Most reporting problems are not reporting problems. They are definition problems wearing a dashboard.
The hard part was never plotting the number. It was getting everyone to agree what the number was, then putting that agreement somewhere only code can edit.
Screen to come
The summary screen, one set of numbers
Screen to come
The same numbers on a phone, read standing up
Like
Like this project

