
Crossdeck
Revenue, analytics, errors, and entitlements in one customer
This week we shipped the first endpoints of an outgoing read API. The idea is simple: your revenue, your errors, your read-cost, and the cross-match (who and what spent each database read) shouldn't be trapped inside our dashboard. You can pull them into your own backend and render them inside your own product.
Building the access was the easy half. The half that took the real thinking was restraint.
Take the revenue endpoint. The obvious version returns everything — every charge, every customer, maximum granularity. We didn't ship that. /v1/revenue returns aggregates only: MRR, paying-customer counts, revenue split by rail (Stripe, Apple, Google), an optional daily series. No named customer. No individual charge row.
Two reasons. First, financial data is the highest-sensitivity surface we touch — the cost of getting an export wrong there isn't symmetric with the convenience of getting it right. Second, secret-key only: the browser never talks to this API, a publishable key is rejected outright. If data is leaving the building, it leaves from your server, scoped to your project, fail-closed by default.
The lesson we keep relearning: when you build the door that lets data out, the design work is in what you refuse, not what you allow. Access is a feature anyone can add. Restraint is the thing customers actually trust.
And there's a quieter point underneath it. The same logic that makes a platform safe to build on is what makes it safe to leave. If pulling your data out is easy, staying is a choice you keep making — not a cage you're stuck in. Locking data in feels like leverage. In our experience it's fragility wearing a costume.
If you're building anything multi-tenant: ship the export early, and spend most of your time on the subtractions.
Most LTV calculations start the way everyone sketches it on a whiteboard: list price × months active. Clean, fast, and quietly optimistic.
The problem is what that formula assumes — that every month you "have" a customer, you actually got paid. You didn't. Cards fail. Subscriptions lapse mid-cycle. You issue a refund three weeks after the charge. Recurring revenue runs on a clock, and a real number of those ticks never clear the bank. List-price LTV ignores all of that and hands you a flattering total.
The honest version comes from the ledger, not the price sheet:
LTV per customer = net succeeded charges − refunds.
Not what you invoiced. What actually settled.
Recurring and one-off roll up the same way — from money that cleared, not money you're owed.
Why it matters: the convenient version of a number doesn't just lose precision, it points you the wrong way. If list-price LTV says a cohort is healthy and the ledger says it churned in month two, you'll keep spending to acquire more of it.
The ledger-true number is usually smaller. It's also the only LTV figure worth defending — to yourself, or to anyone reading the books.
Rule of thumb: if a revenue metric was defined for convenience, assume it's flattering you until you've checked it against what actually cleared.
1 Like
Comment
A few weeks ago I opened Firebase and saw a number that made me uncomfortable.
Almost a million reads a day.
Firebase could tell me the total.
What it couldn't tell me was why.
Was it a user feature?
A background job?
A bug?
A dashboard page nobody even used?
The deeper I dug, the stranger it got.
One analytics tile was responsible for 167,000 reads a day.
Another was responsible for 161,000.
One path in our ingest pipeline was consuming over a million reads a day on its own.
The frustrating part wasn't fixing it.
The frustrating part was finding it.
Every tool we looked at could tell us a query was running.
None could tell us which feature was responsible, which user triggered it, or whether the reads were coming from a person or a machine.
So we built a small internal tool.
Instead of instrumenting individual queries, it sits at the datastore SDK boundary and counts everything automatically.
Every read is attributed to:
the feature that spent it
the user that triggered it
the environment it came from
A few fixes later, we watched one path drop by 396,000 reads per day.
That's when we realised this wasn't just our problem.
So we open-sourced it.
It's called Buckets.
I'm curious: how are other founders tracking database costs today?
When your bill spikes, how do you figure out which feature actually caused it?
1 Like
Comment
I didn't start Crossdeck because I wanted to build another analytics tool.
I started it because I was running subscription apps and couldn't answer simple questions quickly.
How much revenue did we make today?
Which customers are actually using the product?
Did a bug affect paying customers or free users?
Why did this customer lose access?
The answers were scattered across multiple tools. Revenue in one place. Analytics in another. Errors somewhere else. Entitlements hidden behind APIs and dashboards.
The breaking point was realizing I was spending more time stitching together answers than acting on them.
So I started building Crossdeck.
The idea is simple: every purchase, entitlement change, event, and error should live on the same customer timeline.
Not because dashboards are interesting.
Because when a customer writes in, something breaks, or revenue changes, you need answers immediately.
We're still early, but it's already become the system I wish I'd had when I launched my first subscription app.
Curious how other founders are handling subscriptions, analytics, and entitlements today. Are you using one platform, or stitching multiple tools together?
1 Like
Comment
About
I built Crossdeck because I was tired of stitching together billing, analytics, error monitoring, and entitlement systems across my apps. I wanted one source of truth for revenue, customer & behaviour.

Comment