Modeling revenue metrics
Turn payment/subscription data into durable revenue models. Read modeling-warehouse-foundations first for
the view-vs-dbt decision, the view-* workflow, and convertCurrency(); this skill is the revenue-specific
layer on top. Metric definitions live in
references/revenue-metric-definitions.md; copy-paste recipes in
references/posthog/ and references/dbt/.
Step 1 — find where revenue lives
Revenue reaches PostHog two ways; both feed the same managed revenue_analytics_* views:
- A payment platform as a warehouse source — Stripe today (Chargebee/Polar/RevenueCat coming). Best when
the business runs on a billing platform. Connect via
setting-up-a-data-warehouse-source.
- Custom revenue events — you send events (e.g.
purchase_completed) with a revenue property. Best when
there's no supported platform or you already track revenue in-product.
If neither exists yet, use suggesting-data-imports to recommend a source. In dbt, the equivalent is
staging whichever billing tables landed in the warehouse.
Step 2 — model on the managed views, not raw tables
PostHog auto-generates a curated set of views per source. Do not re-derive revenue from raw Stripe
tables — the managed views already handle deferred-revenue recognition, currency, and a stable schema.
Discover the exact names (they're prefixed by source, e.g. stripe.<prefix>.…, plus a cross-source
revenue_analytics.all.…):
SELECT table_name FROM system.information_schema.tables WHERE table_name ILIKE '%revenue_analytics%'
| Managed view |
Grain |
Use for |
revenue_item (start here) |
1 / invoice line item |
Gross revenue, monthly recurring revenue, revenue by product/customer/period. Implements deferred revenue + currency. |
mrr |
1 / (customer, subscription) |
Live snapshot of current MRR — not a time series. |
customer |
1 / customer |
dim_customer: email, country, cohort, metadata. |
subscription |
1 / subscription |
Subscription state for churn/expansion logic. |
charge |
1 / charge |
Raw charges; prefer revenue_item unless you specifically need charges. |
product |
1 / product |
Product dimension. |
Key revenue_item columns: amount (already converted to the project base currency), currency (that
base currency), original_amount / original_currency (as charged), is_recurring, customer_id,
subscription_id, product_id, group_0_key…group_4_key (B2B account keys), timestamp.
Rules before you model (revenue gotchas)
- MRR is empty without a subscription config. For event-based revenue, MRR only populates when a
subscription property is configured. Empty MRR + populated gross revenue is expected behaviour, not a
bug — say so instead of "fixing" it.
- The
mrr managed view is a current snapshot, not history ("MRR at the current time"). For MRR over
time, sum recurring amount per month from revenue_item (see the recipe), or materialize a monthly
snapshot of the mrr view on a schedule.
amount is already in base currency. Use it directly for reporting. Only call
convertCurrency(original_currency, 'XXX', original_amount, timestamp) when you need a different target
currency, or when working from raw events.
- Link revenue to people via metadata. Person/group-level revenue needs
posthog_person_distinct_id metadata on the Stripe customer (or the person join). Without it, revenue is
customer-level only.
- Exclude test accounts. Confirm
filter_test_accounts behaviour so QA/internal charges don't inflate
revenue.
Step 3 — build the model
PostHog: write the HogQL (alias every column), view-create, verify with view-get, then
view-materialize the expensive monthly rollups (a daily sync_frequency is usually right for revenue).
Recipes: references/posthog/ — mrr_and_arr.sql, gross_revenue_by_month.sql,
revenue_by_customer.sql.
dbt: stage the billing source → fct_revenue_item, fct_mrr, dim_customer marts with tests.
Recipes: references/dbt/. Note dbt has no convertCurrency() — supply a rate seed.
Then register the model (references/governance.md in foundations): annotate columns and, if MRR/ARR is a
headline number, propose it to the semantic layer.
File map
| File |
Read when |
references/revenue-metric-definitions.md |
Precise definitions: MRR, ARR, gross, new/expansion/contraction/churn, ARPU, LTV. |
references/posthog/ |
HogQL view recipes on the managed views. |
references/dbt/ |
dbt staging + fct_*/dim_* marts + schema.yml tests. |
Companions
modeling-warehouse-foundations (mechanics), setting-up-a-data-warehouse-source +
suggesting-data-imports (get Stripe/revenue data in), modeling-dimension-tables (currency/plan
dimensions), querying-posthog-data (HogQL + the semantic-layer metric check).
1---2name: modeling-revenue-metrics3description: Build reusable revenue models — MRR, ARR, gross revenue, new/expansion/contraction/churn, ARPU, LTV, and per-customer/per-account revenue — on either PostHog data-warehouse views (HogQL) or an external dbt project. Use when the user wants to model, define, or compute recurring revenue, monthly/annual recurring revenue, churn or retention of revenue, lifetime value, average revenue per user, or revenue by customer, cohort, product, or currency. On PostHog, build on the managed revenue_analytics_* views (revenue_item, mrr, customer, subscription, charge, product) fed by Stripe or custom revenue events — not raw Stripe tables — and normalize money with convertCurrency(). In dbt, stage the payment source and compute fct_mrr / fct_revenue_item / dim_customer marts with tests. Covers picking the right source, the subscription-config gotcha that leaves MRR empty, currency handling, and linking revenue to persons/groups. Read modeling-warehouse-foundations first for the view-vs-dbt mechanics.4---56# Modeling revenue metrics78Turn payment/subscription data into durable revenue models. Read `modeling-warehouse-foundations` first for9the view-vs-dbt decision, the `view-*` workflow, and `convertCurrency()`; this skill is the revenue-specific10layer on top. Metric definitions live in11[`references/revenue-metric-definitions.md`](references/revenue-metric-definitions.md); copy-paste recipes in12[`references/posthog/`](references/posthog/) and [`references/dbt/`](references/dbt/).1314## Step 1 — find where revenue lives1516Revenue reaches PostHog two ways; both feed the same **managed `revenue_analytics_*` views**:1718- **A payment platform as a warehouse source** — Stripe today (Chargebee/Polar/RevenueCat coming). Best when19 the business runs on a billing platform. Connect via `setting-up-a-data-warehouse-source`.20- **Custom revenue events** — you send events (e.g. `purchase_completed`) with a revenue property. Best when21 there's no supported platform or you already track revenue in-product.2223If neither exists yet, use `suggesting-data-imports` to recommend a source. In **dbt**, the equivalent is24staging whichever billing tables landed in the warehouse.2526## Step 2 — model on the managed views, not raw tables2728PostHog auto-generates a curated set of views per source. **Do not re-derive revenue from raw Stripe29tables** — the managed views already handle deferred-revenue recognition, currency, and a stable schema.3031Discover the exact names (they're prefixed by source, e.g. `stripe.<prefix>.…`, plus a cross-source32`revenue_analytics.all.…`):3334```sql35SELECT table_name FROM system.information_schema.tables WHERE table_name ILIKE '%revenue_analytics%'36```3738| Managed view | Grain | Use for |39| ------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |40| `revenue_item` (**start here**) | 1 / invoice line item | Gross revenue, monthly recurring revenue, revenue by product/customer/period. Implements deferred revenue + currency. |41| `mrr` | 1 / (customer, subscription) | **Live snapshot** of current MRR — not a time series. |42| `customer` | 1 / customer | `dim_customer`: email, country, cohort, metadata. |43| `subscription` | 1 / subscription | Subscription state for churn/expansion logic. |44| `charge` | 1 / charge | Raw charges; prefer `revenue_item` unless you specifically need charges. |45| `product` | 1 / product | Product dimension. |4647Key `revenue_item` columns: `amount` (already converted to the project **base currency**), `currency` (that48base currency), `original_amount` / `original_currency` (as charged), `is_recurring`, `customer_id`,49`subscription_id`, `product_id`, `group_0_key`…`group_4_key` (B2B account keys), `timestamp`.5051## Rules before you model (revenue gotchas)52531. **MRR is empty without a subscription config.** For event-based revenue, MRR only populates when a54 subscription property is configured. Empty MRR + populated gross revenue is **expected behaviour**, not a55 bug — say so instead of "fixing" it.562. **The `mrr` managed view is a current snapshot**, not history ("MRR at the current time"). For MRR _over57 time_, sum recurring `amount` per month from `revenue_item` (see the recipe), or materialize a monthly58 snapshot of the `mrr` view on a schedule.593. **`amount` is already in base currency.** Use it directly for reporting. Only call60 `convertCurrency(original_currency, 'XXX', original_amount, timestamp)` when you need a _different_ target61 currency, or when working from raw events.624. **Link revenue to people via metadata.** Person/group-level revenue needs63 `posthog_person_distinct_id` metadata on the Stripe customer (or the person join). Without it, revenue is64 customer-level only.655. **Exclude test accounts.** Confirm `filter_test_accounts` behaviour so QA/internal charges don't inflate66 revenue.6768## Step 3 — build the model6970**PostHog:** write the HogQL (alias every column), `view-create`, verify with `view-get`, then71`view-materialize` the expensive monthly rollups (a daily `sync_frequency` is usually right for revenue).72Recipes: [`references/posthog/`](references/posthog/) — `mrr_and_arr.sql`, `gross_revenue_by_month.sql`,73`revenue_by_customer.sql`.7475**dbt:** stage the billing source → `fct_revenue_item`, `fct_mrr`, `dim_customer` marts with tests.76Recipes: [`references/dbt/`](references/dbt/). Note dbt has no `convertCurrency()` — supply a rate seed.7778Then register the model (`references/governance.md` in foundations): annotate columns and, if MRR/ARR is a79headline number, propose it to the semantic layer.8081## File map8283| File | Read when |84| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |85| [`references/revenue-metric-definitions.md`](references/revenue-metric-definitions.md) | Precise definitions: MRR, ARR, gross, new/expansion/contraction/churn, ARPU, LTV. |86| [`references/posthog/`](references/posthog/) | HogQL view recipes on the managed views. |87| [`references/dbt/`](references/dbt/) | dbt staging + `fct_*`/`dim_*` marts + `schema.yml` tests. |8889## Companions9091`modeling-warehouse-foundations` (mechanics), `setting-up-a-data-warehouse-source` +92`suggesting-data-imports` (get Stripe/revenue data in), `modeling-dimension-tables` (currency/plan93dimensions), `querying-posthog-data` (HogQL + the semantic-layer metric check).