Vibecode ChartMogul
track this build4 phases, 12 steps, beginner friendly0%The visible subscription analytics loop is buildable, but a credible replacement needs more than the first screen. ChartMogul earns its keep through ingestion, storage, reliability, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of ChartMogul. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # ChartMogul · indie build A subscription analytics layer over billing data whose difficulty is definitional rather than computational: the arithmetic is easy, the disagreement about what counts is not, and every number has to carry its own definition to be worth anything. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Source | Read-only import from the billing system, never a second source of truth | the billing system charged the money and is authoritative | | Amounts | Integer minor units with currency and the rate at the time | converting historic revenue at today's rate rewrites history | | Definitions | Every metric stored as an explicit rule set with a version | the arithmetic is trivial and the definitions are the product | | Movements | Revenue changes classified into new, expansion, contraction, churn and reactivation | the categories are where definitions bite hardest | | Traceability | Every figure openable to the subscriptions and invoices behind it | the only way to settle a disagreement | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the import and the metric computation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Movements, definitions and exact integer arithmetic over billing history. - Get it: Docker or your package manager. - Verify: psql connects and integer sums are exact - [ ] **Read access to a real billing system** · free with an existing account - Why: Failed payments, retries, mid-period changes and refunds are the cases that break naive imports, and none of them appear in generated data. - Get it: Read-only API access to a billing account with at least a year of history including refunds and failed payments. - Verify: Imported totals reconcile against the system's own report - [ ] **Your five definitional decisions, written first** · free, and it takes a conversation - Why: Annual spreading, discounts, refunds, set-up fees and usage each change the figures materially, and deciding them after seeing the numbers means choosing whichever looks best. - Get it: Write down how each is treated and why, and agree it with whoever will quote the figures. - Verify: A written definition covers all five and appears in the configuration - [ ] **A year of billing including awkward cases** · free - Why: Movement classification is only testable where a real customer downgraded then upgraded, or churned and returned, which generated data never contains. - Get it: Use the real history rather than a fixture, and identify the awkward customers before computing anything. - Verify: At least three customers with mid-month changes or returns are identified ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Being the billing system. - Accounting revenue recognition. - Forecasting. - Reconciling across several billing systems. - session replay privacy tooling - enterprise governance and integrations - high-volume ingestion and retention - bot and identity resolution If one of those is essential to you, that is the reason to keep paying for ChartMogul, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · ChartMogul The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me subscription revenue metrics where every figure states how it was defined to replace ChartMogul. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero There is no standard definition of recurring revenue, and that is the entire difficulty. An annual contract paid upfront can be counted as a twelfth per month or as revenue in the month it was paid. A discount can reduce recurring revenue or be reported separately. A refund can reverse the month it belonged to or the month it was issued. A trial converting mid-month can count fully, proportionally, or from the following month. Set-up fees and usage charges may or may not be recurring at all. Every one of those is defensible and they produce materially different figures from the same billing data. Two tools will disagree, and so will you and your investor, and the argument is unresolvable unless the definitions are written down. So each metric here stores its definition as data, displays it beside the figure, and records when a definition changed so a series that shifts can be explained by the change rather than mistaken for the business moving. ### Stack (fixed, do not substitute) - Source: Read-only import from the billing system, never a second source of truth · the billing system charged the money and is authoritative - Amounts: Integer minor units with currency and the rate at the time · converting historic revenue at today's rate rewrites history - Definitions: Every metric stored as an explicit rule set with a version · the arithmetic is trivial and the definitions are the product - Movements: Revenue changes classified into new, expansion, contraction, churn and reactivation · the categories are where definitions bite hardest - Traceability: Every figure openable to the subscriptions and invoices behind it · the only way to settle a disagreement ### Data model (create this before Phase 1) - customers(id, external_id, first_paid_at, churned_at, segments_json) - subscriptions(id, customer_id, plan_id, started_at, ended_at, interval, amount_minor, currency, discount_json) - invoices(id, customer_id, issued_at, paid_at, refunded_at, lines_json, amount_minor, currency, fx_rate) - metric_definitions(id, name, version, rules_json, note, effective_from) · what counts, written down - movements(month, customer_id, kind, amount_minor, definition_version) · kind is new, expansion, contraction, churn or reactivation - metric_values(metric_id, definition_version, period, value_minor, components_json, computed_at) - A movement records the definition version that classified it, so a series computed under two definitions is visibly two series rather than one that jumped. ### Phase 1 · Importing billing without becoming billing Build: Import customers, subscriptions and invoices read only. Reconcile totals against the billing system's own reports. Handle a subscription changed mid-period, a refund and a failed payment that later succeeded. Trap: Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. Done when: Imported invoice totals reconcile exactly against the billing system's own report for a period. A failed payment that later succeeded does not produce a churn followed by a reactivation. A mid-period plan change is imported with both the old and new amounts. Re-importing is idempotent and changes nothing. Do not build yet: No definitions, metrics or movements. ### Phase 2 · Definitions as data Build: Store each metric's definition as explicit rules: how annual contracts are spread, how discounts are treated, how refunds are attributed, what counts as recurring. Version definitions and display the current one beside every figure. Trap: Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. Done when: Each metric has a stored rule set covering annual spreading, discounts, refunds, set-up fees and usage. The definition is displayed beside every figure rather than in documentation. Changing a definition creates a new version rather than editing the old one. Two metrics with the same name and different definitions are impossible or clearly distinguished. Do not build yet: No movements, cohorts or reporting yet. ### Phase 3 · Movements, where definitions bite Build: Classify month-over-month revenue changes per customer into new, expansion, contraction, churn and reactivation, recording the definition version used. Handle a customer who downgrades and upgrades in one month, and one who churns and returns. Trap: A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. Done when: Movements are classified with the definition version recorded per movement. A customer with both a downgrade and an upgrade in one month is handled by the stated rule and the rule is visible. Movements sum exactly to the change in recurring revenue for the period, asserted in a test. A churn followed by a return is classified by the stated reactivation window. Do not build yet: No cohorts, currency handling or reporting yet. ### Phase 4 · More than one currency Build: Store each amount in its own currency with the rate at the time of the invoice. Convert for reporting using the stored rate rather than the current one. State the rate policy beside converted figures. Trap: Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. Done when: Historic figures do not change when exchange rates move, verified by changing a rate and re-running a past period. Each amount retains its own currency and the rate applied. The rate policy is stated beside every converted figure. A customer whose currency changed mid-relationship is handled without corrupting their history. Do not build yet: No cohorts or reporting yet. ### Phase 5 · Cohorts, segments and settling arguments Build: Cohort retention and segment breakdowns computed from the same definitions. Make every figure drillable to the subscriptions and invoices behind it, and assert the detail sums to the figure. Export with definitions included. Trap: A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements. Done when: Every figure drills to its underlying subscriptions and invoices and the detail sums exactly to the figure, asserted across metrics. Cohorts and segments use the same definitions as headline figures, verified by reconciliation. Export includes the definitions used so a figure can be reproduced elsewhere. A definition change is visible as a break in the series rather than as a jump. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the billing system, which charged the money and remains authoritative. - Revenue recognition for accounting purposes, which follows standards this does not implement and should not be confused with. - Forecasting, which requires assumptions this build deliberately does not make on your behalf. - Reconciling across several billing systems, which is a data problem considerably larger than the metrics. ### README must contain - That there is no standard definition of recurring revenue, and five specific decisions change every figure - That each metric's definition is stored as data and displayed beside its figure - That definition changes create versions, so a series break is explicable rather than a jump - That exchange rates are fixed at the invoice, so historic figures do not move - That every figure drills to detail that sums to it, asserted in the suite ===== AGENTS.md ===== # Agent instructions · ChartMogul indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Read-only import from the billing system, never a second source of truth, Integer minor units with currency and the rate at the time, Every metric stored as an explicit rule set with a version, Revenue changes classified into new, expansion, contraction, churn and reactivation, Every figure openable to the subscriptions and invoices behind it. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. ===== BUILD_PLAN.md ===== # Build plan · ChartMogul A subscription analytics layer over billing data whose difficulty is definitional rather than computational: the arithmetic is easy, the disagreement about what counts is not, and every number has to carry its own definition to be worth anything. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing billing without becoming billing Every charge, refund and change is imported and reconciles to the source. ### Steps 1. Import the full event history Failures and retries, not only successful charges. Files: `src/import.ts` 2. Reconcile against the source's own report Exactly. A difference is a defect rather than a rounding matter. Files: `tests/reconcile.test.ts` 3. Make import idempotent Re-running must change nothing. Files: `src/import.ts` ### Done when - [ ] Totals reconcile exactly against the billing system - [ ] A retried payment does not produce churn and reactivation - [ ] Mid-period changes import both amounts - [ ] Re-importing changes nothing ### Watch out - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. ## Phase 2 · Definitions as data Every metric has a written rule set you can read. ### Steps 1. Write the rules as data Annual spreading, discounts, refunds, fees, usage. Five decisions minimum. Files: `config/definitions.json` 2. Display the definition with the figure So a disagreement is about the rule rather than the arithmetic. Files: `src/report.ts` 3. Version rather than edit A changed definition changes history and must be visible. Files: `src/definitions.ts` ### Done when - [ ] Each metric has a stored rule set covering five decisions - [ ] Definitions display beside figures - [ ] Changes create versions rather than edits - [ ] Same-named metrics with different rules are distinguished ### Watch out - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. ## Phase 3 · Movements, where definitions bite Every change in revenue is classified consistently and traceably. ### Steps 1. Classify against the stated rules Recording which version classified each movement. Files: `src/movements.ts` 2. Assert movements sum to the change If they do not, one of the categories is wrong. Files: `tests/movement-sum.test.ts` 3. State the reactivation window A customer returning after two months is a different event from one after two years. Files: `src/movements.ts` ### Done when - [ ] Movements record the definition version applied - [ ] A downgrade and upgrade in one month follows the stated rule - [ ] Movements sum exactly to the period's change - [ ] Reactivation follows a stated window ### Watch out - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. ## Phase 4 · More than one currency Historic revenue does not change when exchange rates move. ### Steps 1. Fix the rate at the invoice Re-converting rewrites history every morning. Files: `src/currency.ts` 2. State the policy beside conversions So the figure can be reproduced by someone else. Files: `src/report.ts` 3. Handle a currency change mid-relationship It happens and it should not corrupt the customer's history. Files: `src/currency.ts` ### Done when - [ ] Historic figures are unaffected by rate changes - [ ] Each amount keeps its currency and rate - [ ] The rate policy is stated beside conversions - [ ] A mid-relationship currency change is handled ### Watch out - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. ## Not in this build - Being the billing system. - Accounting revenue recognition. - Forecasting. - Reconciling across several billing systems. ## After v1, if you want it - Segment comparison with the same definitions applied, which is where the questions go next - A definition change preview showing what a series would look like under a new rule - Customer-level revenue history, which is what account managers actually ask for ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Imported billing history for a real business. DATABASE_URL=postgres://localhost:5432/metrics # Required · secret. Read only. This never writes to the billing system. BILLING_API_KEY=a read-only key # Required. The five decisions, versioned, displayed beside every figure. DEFINITIONS_PATH=./config/definitions.json # Required. For conversion. Source amounts keep their own currency and rate. REPORTING_CURRENCY=USD # Required. Present as a constant. Re-converting rewrites history every morning. FX_POLICY=rate_at_invoice # Required. A returning customer within this is reactivation rather than new. REACTIVATION_WINDOW_DAYS=90
You are building a lean indie version of ChartMogul. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # ChartMogul · indie build A subscription analytics layer over billing data whose difficulty is definitional rather than computational: the arithmetic is easy, the disagreement about what counts is not, and every number has to carry its own definition to be worth anything. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Source | Read-only import from the billing system, never a second source of truth | the billing system charged the money and is authoritative | | Amounts | Integer minor units with currency and the rate at the time | converting historic revenue at today's rate rewrites history | | Definitions | Every metric stored as an explicit rule set with a version | the arithmetic is trivial and the definitions are the product | | Movements | Revenue changes classified into new, expansion, contraction, churn and reactivation | the categories are where definitions bite hardest | | Traceability | Every figure openable to the subscriptions and invoices behind it | the only way to settle a disagreement | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the import and the metric computation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Movements, definitions and exact integer arithmetic over billing history. - Get it: Docker or your package manager. - Verify: psql connects and integer sums are exact - [ ] **Read access to a real billing system** · free with an existing account - Why: Failed payments, retries, mid-period changes and refunds are the cases that break naive imports, and none of them appear in generated data. - Get it: Read-only API access to a billing account with at least a year of history including refunds and failed payments. - Verify: Imported totals reconcile against the system's own report - [ ] **Your five definitional decisions, written first** · free, and it takes a conversation - Why: Annual spreading, discounts, refunds, set-up fees and usage each change the figures materially, and deciding them after seeing the numbers means choosing whichever looks best. - Get it: Write down how each is treated and why, and agree it with whoever will quote the figures. - Verify: A written definition covers all five and appears in the configuration - [ ] **A year of billing including awkward cases** · free - Why: Movement classification is only testable where a real customer downgraded then upgraded, or churned and returned, which generated data never contains. - Get it: Use the real history rather than a fixture, and identify the awkward customers before computing anything. - Verify: At least three customers with mid-month changes or returns are identified ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Being the billing system. - Accounting revenue recognition. - Forecasting. - Reconciling across several billing systems. - session replay privacy tooling - enterprise governance and integrations - high-volume ingestion and retention - bot and identity resolution If one of those is essential to you, that is the reason to keep paying for ChartMogul, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · ChartMogul The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me subscription revenue metrics where every figure states how it was defined to replace ChartMogul. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero There is no standard definition of recurring revenue, and that is the entire difficulty. An annual contract paid upfront can be counted as a twelfth per month or as revenue in the month it was paid. A discount can reduce recurring revenue or be reported separately. A refund can reverse the month it belonged to or the month it was issued. A trial converting mid-month can count fully, proportionally, or from the following month. Set-up fees and usage charges may or may not be recurring at all. Every one of those is defensible and they produce materially different figures from the same billing data. Two tools will disagree, and so will you and your investor, and the argument is unresolvable unless the definitions are written down. So each metric here stores its definition as data, displays it beside the figure, and records when a definition changed so a series that shifts can be explained by the change rather than mistaken for the business moving. ### Stack (fixed, do not substitute) - Source: Read-only import from the billing system, never a second source of truth · the billing system charged the money and is authoritative - Amounts: Integer minor units with currency and the rate at the time · converting historic revenue at today's rate rewrites history - Definitions: Every metric stored as an explicit rule set with a version · the arithmetic is trivial and the definitions are the product - Movements: Revenue changes classified into new, expansion, contraction, churn and reactivation · the categories are where definitions bite hardest - Traceability: Every figure openable to the subscriptions and invoices behind it · the only way to settle a disagreement ### Data model (create this before Phase 1) - customers(id, external_id, first_paid_at, churned_at, segments_json) - subscriptions(id, customer_id, plan_id, started_at, ended_at, interval, amount_minor, currency, discount_json) - invoices(id, customer_id, issued_at, paid_at, refunded_at, lines_json, amount_minor, currency, fx_rate) - metric_definitions(id, name, version, rules_json, note, effective_from) · what counts, written down - movements(month, customer_id, kind, amount_minor, definition_version) · kind is new, expansion, contraction, churn or reactivation - metric_values(metric_id, definition_version, period, value_minor, components_json, computed_at) - A movement records the definition version that classified it, so a series computed under two definitions is visibly two series rather than one that jumped. ### Phase 1 · Importing billing without becoming billing Build: Import customers, subscriptions and invoices read only. Reconcile totals against the billing system's own reports. Handle a subscription changed mid-period, a refund and a failed payment that later succeeded. Trap: Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. Done when: Imported invoice totals reconcile exactly against the billing system's own report for a period. A failed payment that later succeeded does not produce a churn followed by a reactivation. A mid-period plan change is imported with both the old and new amounts. Re-importing is idempotent and changes nothing. Do not build yet: No definitions, metrics or movements. ### Phase 2 · Definitions as data Build: Store each metric's definition as explicit rules: how annual contracts are spread, how discounts are treated, how refunds are attributed, what counts as recurring. Version definitions and display the current one beside every figure. Trap: Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. Done when: Each metric has a stored rule set covering annual spreading, discounts, refunds, set-up fees and usage. The definition is displayed beside every figure rather than in documentation. Changing a definition creates a new version rather than editing the old one. Two metrics with the same name and different definitions are impossible or clearly distinguished. Do not build yet: No movements, cohorts or reporting yet. ### Phase 3 · Movements, where definitions bite Build: Classify month-over-month revenue changes per customer into new, expansion, contraction, churn and reactivation, recording the definition version used. Handle a customer who downgrades and upgrades in one month, and one who churns and returns. Trap: A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. Done when: Movements are classified with the definition version recorded per movement. A customer with both a downgrade and an upgrade in one month is handled by the stated rule and the rule is visible. Movements sum exactly to the change in recurring revenue for the period, asserted in a test. A churn followed by a return is classified by the stated reactivation window. Do not build yet: No cohorts, currency handling or reporting yet. ### Phase 4 · More than one currency Build: Store each amount in its own currency with the rate at the time of the invoice. Convert for reporting using the stored rate rather than the current one. State the rate policy beside converted figures. Trap: Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. Done when: Historic figures do not change when exchange rates move, verified by changing a rate and re-running a past period. Each amount retains its own currency and the rate applied. The rate policy is stated beside every converted figure. A customer whose currency changed mid-relationship is handled without corrupting their history. Do not build yet: No cohorts or reporting yet. ### Phase 5 · Cohorts, segments and settling arguments Build: Cohort retention and segment breakdowns computed from the same definitions. Make every figure drillable to the subscriptions and invoices behind it, and assert the detail sums to the figure. Export with definitions included. Trap: A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements. Done when: Every figure drills to its underlying subscriptions and invoices and the detail sums exactly to the figure, asserted across metrics. Cohorts and segments use the same definitions as headline figures, verified by reconciliation. Export includes the definitions used so a figure can be reproduced elsewhere. A definition change is visible as a break in the series rather than as a jump. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the billing system, which charged the money and remains authoritative. - Revenue recognition for accounting purposes, which follows standards this does not implement and should not be confused with. - Forecasting, which requires assumptions this build deliberately does not make on your behalf. - Reconciling across several billing systems, which is a data problem considerably larger than the metrics. ### README must contain - That there is no standard definition of recurring revenue, and five specific decisions change every figure - That each metric's definition is stored as data and displayed beside its figure - That definition changes create versions, so a series break is explicable rather than a jump - That exchange rates are fixed at the invoice, so historic figures do not move - That every figure drills to detail that sums to it, asserted in the suite ===== AGENTS.md ===== # Agent instructions · ChartMogul indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Read-only import from the billing system, never a second source of truth, Integer minor units with currency and the rate at the time, Every metric stored as an explicit rule set with a version, Revenue changes classified into new, expansion, contraction, churn and reactivation, Every figure openable to the subscriptions and invoices behind it. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. ===== BUILD_PLAN.md ===== # Build plan · ChartMogul A subscription analytics layer over billing data whose difficulty is definitional rather than computational: the arithmetic is easy, the disagreement about what counts is not, and every number has to carry its own definition to be worth anything. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing billing without becoming billing Every charge, refund and change is imported and reconciles to the source. ### Steps 1. Import the full event history Failures and retries, not only successful charges. Files: `src/import.ts` 2. Reconcile against the source's own report Exactly. A difference is a defect rather than a rounding matter. Files: `tests/reconcile.test.ts` 3. Make import idempotent Re-running must change nothing. Files: `src/import.ts` ### Done when - [ ] Totals reconcile exactly against the billing system - [ ] A retried payment does not produce churn and reactivation - [ ] Mid-period changes import both amounts - [ ] Re-importing changes nothing ### Watch out - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. ## Phase 2 · Definitions as data Every metric has a written rule set you can read. ### Steps 1. Write the rules as data Annual spreading, discounts, refunds, fees, usage. Five decisions minimum. Files: `config/definitions.json` 2. Display the definition with the figure So a disagreement is about the rule rather than the arithmetic. Files: `src/report.ts` 3. Version rather than edit A changed definition changes history and must be visible. Files: `src/definitions.ts` ### Done when - [ ] Each metric has a stored rule set covering five decisions - [ ] Definitions display beside figures - [ ] Changes create versions rather than edits - [ ] Same-named metrics with different rules are distinguished ### Watch out - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. ## Phase 3 · Movements, where definitions bite Every change in revenue is classified consistently and traceably. ### Steps 1. Classify against the stated rules Recording which version classified each movement. Files: `src/movements.ts` 2. Assert movements sum to the change If they do not, one of the categories is wrong. Files: `tests/movement-sum.test.ts` 3. State the reactivation window A customer returning after two months is a different event from one after two years. Files: `src/movements.ts` ### Done when - [ ] Movements record the definition version applied - [ ] A downgrade and upgrade in one month follows the stated rule - [ ] Movements sum exactly to the period's change - [ ] Reactivation follows a stated window ### Watch out - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. ## Phase 4 · More than one currency Historic revenue does not change when exchange rates move. ### Steps 1. Fix the rate at the invoice Re-converting rewrites history every morning. Files: `src/currency.ts` 2. State the policy beside conversions So the figure can be reproduced by someone else. Files: `src/report.ts` 3. Handle a currency change mid-relationship It happens and it should not corrupt the customer's history. Files: `src/currency.ts` ### Done when - [ ] Historic figures are unaffected by rate changes - [ ] Each amount keeps its currency and rate - [ ] The rate policy is stated beside conversions - [ ] A mid-relationship currency change is handled ### Watch out - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. ## Not in this build - Being the billing system. - Accounting revenue recognition. - Forecasting. - Reconciling across several billing systems. ## After v1, if you want it - Segment comparison with the same definitions applied, which is where the questions go next - A definition change preview showing what a series would look like under a new rule - Customer-level revenue history, which is what account managers actually ask for ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Imported billing history for a real business. DATABASE_URL=postgres://localhost:5432/metrics # Required · secret. Read only. This never writes to the billing system. BILLING_API_KEY=a read-only key # Required. The five decisions, versioned, displayed beside every figure. DEFINITIONS_PATH=./config/definitions.json # Required. For conversion. Source amounts keep their own currency and rate. REPORTING_CURRENCY=USD # Required. Present as a constant. Re-converting rewrites history every morning. FX_POLICY=rate_at_invoice # Required. A returning customer within this is reactivation rather than new. REACTIVATION_WINDOW_DAYS=90
You are building a production product version of ChartMogul. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # ChartMogul · product brief ## Problem The visible subscription analytics loop is buildable, but a credible replacement needs more than the first screen. ChartMogul earns its keep through ingestion, storage, reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome Subscription metrics where every number states how it was defined, historic figures never move, and a disagreement about churn is a conversation about a written rule rather than about arithmetic. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - deploy target - ClickHouse or SQLite for low volume - first-party tracking script ## Explicit non-goals for v1 - Being the billing system. - Accounting revenue recognition. - Forecasting. - Reconciling across several billing systems. - session replay privacy tooling - enterprise governance and integrations - high-volume ingestion and retention - bot and identity resolution ## Success criteria - Imported totals are confirmed to reconcile against the billing system - A retried payment is confirmed not to produce churn and reactivation - Movements are confirmed to sum to the period change - Historic figures are confirmed unaffected by rate changes - Every figure is confirmed to drill to reconciling detail ===== BRIEF.md ===== # Build brief · ChartMogul The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me subscription revenue metrics where every figure states how it was defined to replace ChartMogul. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero There is no standard definition of recurring revenue, and that is the entire difficulty. An annual contract paid upfront can be counted as a twelfth per month or as revenue in the month it was paid. A discount can reduce recurring revenue or be reported separately. A refund can reverse the month it belonged to or the month it was issued. A trial converting mid-month can count fully, proportionally, or from the following month. Set-up fees and usage charges may or may not be recurring at all. Every one of those is defensible and they produce materially different figures from the same billing data. Two tools will disagree, and so will you and your investor, and the argument is unresolvable unless the definitions are written down. So each metric here stores its definition as data, displays it beside the figure, and records when a definition changed so a series that shifts can be explained by the change rather than mistaken for the business moving. ### Stack (fixed, do not substitute) - Source: Read-only import from the billing system, never a second source of truth · the billing system charged the money and is authoritative - Amounts: Integer minor units with currency and the rate at the time · converting historic revenue at today's rate rewrites history - Definitions: Every metric stored as an explicit rule set with a version · the arithmetic is trivial and the definitions are the product - Movements: Revenue changes classified into new, expansion, contraction, churn and reactivation · the categories are where definitions bite hardest - Traceability: Every figure openable to the subscriptions and invoices behind it · the only way to settle a disagreement ### Data model (create this before Phase 1) - customers(id, external_id, first_paid_at, churned_at, segments_json) - subscriptions(id, customer_id, plan_id, started_at, ended_at, interval, amount_minor, currency, discount_json) - invoices(id, customer_id, issued_at, paid_at, refunded_at, lines_json, amount_minor, currency, fx_rate) - metric_definitions(id, name, version, rules_json, note, effective_from) · what counts, written down - movements(month, customer_id, kind, amount_minor, definition_version) · kind is new, expansion, contraction, churn or reactivation - metric_values(metric_id, definition_version, period, value_minor, components_json, computed_at) - A movement records the definition version that classified it, so a series computed under two definitions is visibly two series rather than one that jumped. ### Phase 1 · Importing billing without becoming billing Build: Import customers, subscriptions and invoices read only. Reconcile totals against the billing system's own reports. Handle a subscription changed mid-period, a refund and a failed payment that later succeeded. Trap: Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. Done when: Imported invoice totals reconcile exactly against the billing system's own report for a period. A failed payment that later succeeded does not produce a churn followed by a reactivation. A mid-period plan change is imported with both the old and new amounts. Re-importing is idempotent and changes nothing. Do not build yet: No definitions, metrics or movements. ### Phase 2 · Definitions as data Build: Store each metric's definition as explicit rules: how annual contracts are spread, how discounts are treated, how refunds are attributed, what counts as recurring. Version definitions and display the current one beside every figure. Trap: Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. Done when: Each metric has a stored rule set covering annual spreading, discounts, refunds, set-up fees and usage. The definition is displayed beside every figure rather than in documentation. Changing a definition creates a new version rather than editing the old one. Two metrics with the same name and different definitions are impossible or clearly distinguished. Do not build yet: No movements, cohorts or reporting yet. ### Phase 3 · Movements, where definitions bite Build: Classify month-over-month revenue changes per customer into new, expansion, contraction, churn and reactivation, recording the definition version used. Handle a customer who downgrades and upgrades in one month, and one who churns and returns. Trap: A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. Done when: Movements are classified with the definition version recorded per movement. A customer with both a downgrade and an upgrade in one month is handled by the stated rule and the rule is visible. Movements sum exactly to the change in recurring revenue for the period, asserted in a test. A churn followed by a return is classified by the stated reactivation window. Do not build yet: No cohorts, currency handling or reporting yet. ### Phase 4 · More than one currency Build: Store each amount in its own currency with the rate at the time of the invoice. Convert for reporting using the stored rate rather than the current one. State the rate policy beside converted figures. Trap: Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. Done when: Historic figures do not change when exchange rates move, verified by changing a rate and re-running a past period. Each amount retains its own currency and the rate applied. The rate policy is stated beside every converted figure. A customer whose currency changed mid-relationship is handled without corrupting their history. Do not build yet: No cohorts or reporting yet. ### Phase 5 · Cohorts, segments and settling arguments Build: Cohort retention and segment breakdowns computed from the same definitions. Make every figure drillable to the subscriptions and invoices behind it, and assert the detail sums to the figure. Export with definitions included. Trap: A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements. Done when: Every figure drills to its underlying subscriptions and invoices and the detail sums exactly to the figure, asserted across metrics. Cohorts and segments use the same definitions as headline figures, verified by reconciliation. Export includes the definitions used so a figure can be reproduced elsewhere. A definition change is visible as a break in the series rather than as a jump. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the billing system, which charged the money and remains authoritative. - Revenue recognition for accounting purposes, which follows standards this does not implement and should not be confused with. - Forecasting, which requires assumptions this build deliberately does not make on your behalf. - Reconciling across several billing systems, which is a data problem considerably larger than the metrics. ### README must contain - That there is no standard definition of recurring revenue, and five specific decisions change every figure - That each metric's definition is stored as data and displayed beside its figure - That definition changes create versions, so a series break is explicable rather than a jump - That exchange rates are fixed at the invoice, so historic figures do not move - That every figure drills to detail that sums to it, asserted in the suite ===== ARCHITECTURE.md ===== # Architecture · ChartMogul ## Stack | Part | Choice | Why | | --- | --- | --- | | Source | Read-only import from the billing system, never a second source of truth | the billing system charged the money and is authoritative | | Amounts | Integer minor units with currency and the rate at the time | converting historic revenue at today's rate rewrites history | | Definitions | Every metric stored as an explicit rule set with a version | the arithmetic is trivial and the definitions are the product | | Movements | Revenue changes classified into new, expansion, contraction, churn and reactivation | the categories are where definitions bite hardest | | Traceability | Every figure openable to the subscriptions and invoices behind it | the only way to settle a disagreement | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Full event history reconciled to the source. | Any billing system, provided failures and retries are imported rather than only successes. | | Definitions | Versioned rule sets displayed with figures. | Not swappable for definitions in code, which nobody can read when asked. | | Movements | Classification with the version recorded. | Any classifier, provided movements sum exactly to the period change. | | Currency | Rates fixed at the invoice. | Any source, provided historic figures never move. | | Traceability | Drilling that reconciles. | Not swappable. A figure nobody can trace is a figure nobody can defend. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Imported billing history for a real business. - `BILLING_API_KEY` · required, secret · Read only. This never writes to the billing system. - `DEFINITIONS_PATH` · required · The five decisions, versioned, displayed beside every figure. - `REPORTING_CURRENCY` · required · For conversion. Source amounts keep their own currency and rate. - `FX_POLICY` · required · Present as a constant. Re-converting rewrites history every morning. - `REACTIVATION_WINDOW_DAYS` · required · A returning customer within this is reactivation rather than new. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · ChartMogul product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Read-only import from the billing system, never a second source of truth, Integer minor units with currency and the rate at the time, Every metric stored as an explicit rule set with a version, Revenue changes classified into new, expansion, contraction, churn and reactivation, Every figure openable to the subscriptions and invoices behind it. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. - A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements. ===== MILESTONES.md ===== # Delivery milestones · ChartMogul Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Importing billing without becoming billing Every charge, refund and change is imported and reconciles to the source. ### Steps 1. Import the full event history Failures and retries, not only successful charges. Files: `src/import.ts` 2. Reconcile against the source's own report Exactly. A difference is a defect rather than a rounding matter. Files: `tests/reconcile.test.ts` 3. Make import idempotent Re-running must change nothing. Files: `src/import.ts` ### Done when - [ ] Totals reconcile exactly against the billing system - [ ] A retried payment does not produce churn and reactivation - [ ] Mid-period changes import both amounts - [ ] Re-importing changes nothing ### Watch out - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. ## M2 · Definitions as data Every metric has a written rule set you can read. ### Steps 1. Write the rules as data Annual spreading, discounts, refunds, fees, usage. Five decisions minimum. Files: `config/definitions.json` 2. Display the definition with the figure So a disagreement is about the rule rather than the arithmetic. Files: `src/report.ts` 3. Version rather than edit A changed definition changes history and must be visible. Files: `src/definitions.ts` ### Done when - [ ] Each metric has a stored rule set covering five decisions - [ ] Definitions display beside figures - [ ] Changes create versions rather than edits - [ ] Same-named metrics with different rules are distinguished ### Watch out - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. ## M3 · Movements, where definitions bite Every change in revenue is classified consistently and traceably. ### Steps 1. Classify against the stated rules Recording which version classified each movement. Files: `src/movements.ts` 2. Assert movements sum to the change If they do not, one of the categories is wrong. Files: `tests/movement-sum.test.ts` 3. State the reactivation window A customer returning after two months is a different event from one after two years. Files: `src/movements.ts` ### Done when - [ ] Movements record the definition version applied - [ ] A downgrade and upgrade in one month follows the stated rule - [ ] Movements sum exactly to the period's change - [ ] Reactivation follows a stated window ### Watch out - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. ## M4 · More than one currency Historic revenue does not change when exchange rates move. ### Steps 1. Fix the rate at the invoice Re-converting rewrites history every morning. Files: `src/currency.ts` 2. State the policy beside conversions So the figure can be reproduced by someone else. Files: `src/report.ts` 3. Handle a currency change mid-relationship It happens and it should not corrupt the customer's history. Files: `src/currency.ts` ### Done when - [ ] Historic figures are unaffected by rate changes - [ ] Each amount keeps its currency and rate - [ ] The rate policy is stated beside conversions - [ ] A mid-relationship currency change is handled ### Watch out - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. ## M5 · Cohorts, segments and settling arguments (production only) Every figure opens to the customers and invoices behind it. ### Steps 1. Compute everything from one definition set Two definitions produce two authoritative numbers that disagree. Files: `src/cohorts.ts` 2. Assert drilled detail sums to every figure Across every metric, in the suite. Files: `tests/drill-sums.test.ts` 3. Export the definitions with the data So the figures can be reproduced rather than trusted. Files: `src/export.ts` ### Done when - [ ] Every figure drills to reconciling detail - [ ] Cohorts use the same definitions as headline figures - [ ] Export includes the definitions used - [ ] Definition changes appear as series breaks ### Watch out - A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements. ===== OPERATIONS.md ===== # Operations · ChartMogul ## Backup Billing data re-imports from the source, but definition versions and their change history do not, since they record decisions rather than data. Back those up first. ## Restore Restore and confirm definition versions and their effective dates survived, since recomputing under current definitions would silently rewrite historic figures that were already reported. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Reconciliation against the billing system on every import is the health check. A drift means the import is wrong, and every figure after that point is plausible and incorrect. ## Incident checklist A figure quoted externally that no longer reproduces is the failure that matters, and it almost always means either a definition changed without a version or rates were re-applied. Both are visible in the stored versions. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Imported totals are confirmed to reconcile against the billing system - [ ] A retried payment is confirmed not to produce churn and reactivation - [ ] Movements are confirmed to sum to the period change - [ ] Historic figures are confirmed unaffected by rate changes - [ ] Every figure is confirmed to drill to reconciling detail ## Launch constraint Do not market omitted ChartMogul capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Imported billing history for a real business. DATABASE_URL=postgres://localhost:5432/metrics # Required · secret. Read only. This never writes to the billing system. BILLING_API_KEY=a read-only key # Required. The five decisions, versioned, displayed beside every figure. DEFINITIONS_PATH=./config/definitions.json # Required. For conversion. Source amounts keep their own currency and rate. REPORTING_CURRENCY=USD # Required. Present as a constant. Re-converting rewrites history every morning. FX_POLICY=rate_at_invoice # Required. A returning customer within this is reactivation rather than new. REACTIVATION_WINDOW_DAYS=90
# ChartMogul · indie build A subscription analytics layer over billing data whose difficulty is definitional rather than computational: the arithmetic is easy, the disagreement about what counts is not, and every number has to carry its own definition to be worth anything. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Source | Read-only import from the billing system, never a second source of truth | the billing system charged the money and is authoritative | | Amounts | Integer minor units with currency and the rate at the time | converting historic revenue at today's rate rewrites history | | Definitions | Every metric stored as an explicit rule set with a version | the arithmetic is trivial and the definitions are the product | | Movements | Revenue changes classified into new, expansion, contraction, churn and reactivation | the categories are where definitions bite hardest | | Traceability | Every figure openable to the subscriptions and invoices behind it | the only way to settle a disagreement | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the import and the metric computation. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Movements, definitions and exact integer arithmetic over billing history. - Get it: Docker or your package manager. - Verify: psql connects and integer sums are exact - [ ] **Read access to a real billing system** · free with an existing account - Why: Failed payments, retries, mid-period changes and refunds are the cases that break naive imports, and none of them appear in generated data. - Get it: Read-only API access to a billing account with at least a year of history including refunds and failed payments. - Verify: Imported totals reconcile against the system's own report - [ ] **Your five definitional decisions, written first** · free, and it takes a conversation - Why: Annual spreading, discounts, refunds, set-up fees and usage each change the figures materially, and deciding them after seeing the numbers means choosing whichever looks best. - Get it: Write down how each is treated and why, and agree it with whoever will quote the figures. - Verify: A written definition covers all five and appears in the configuration - [ ] **A year of billing including awkward cases** · free - Why: Movement classification is only testable where a real customer downgraded then upgraded, or churned and returned, which generated data never contains. - Get it: Use the real history rather than a fixture, and identify the awkward customers before computing anything. - Verify: At least three customers with mid-month changes or returns are identified ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Being the billing system. - Accounting revenue recognition. - Forecasting. - Reconciling across several billing systems. - session replay privacy tooling - enterprise governance and integrations - high-volume ingestion and retention - bot and identity resolution If one of those is essential to you, that is the reason to keep paying for ChartMogul, and the README should say so rather than pretend.
# Build brief · ChartMogul The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me subscription revenue metrics where every figure states how it was defined to replace ChartMogul. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero There is no standard definition of recurring revenue, and that is the entire difficulty. An annual contract paid upfront can be counted as a twelfth per month or as revenue in the month it was paid. A discount can reduce recurring revenue or be reported separately. A refund can reverse the month it belonged to or the month it was issued. A trial converting mid-month can count fully, proportionally, or from the following month. Set-up fees and usage charges may or may not be recurring at all. Every one of those is defensible and they produce materially different figures from the same billing data. Two tools will disagree, and so will you and your investor, and the argument is unresolvable unless the definitions are written down. So each metric here stores its definition as data, displays it beside the figure, and records when a definition changed so a series that shifts can be explained by the change rather than mistaken for the business moving. ### Stack (fixed, do not substitute) - Source: Read-only import from the billing system, never a second source of truth · the billing system charged the money and is authoritative - Amounts: Integer minor units with currency and the rate at the time · converting historic revenue at today's rate rewrites history - Definitions: Every metric stored as an explicit rule set with a version · the arithmetic is trivial and the definitions are the product - Movements: Revenue changes classified into new, expansion, contraction, churn and reactivation · the categories are where definitions bite hardest - Traceability: Every figure openable to the subscriptions and invoices behind it · the only way to settle a disagreement ### Data model (create this before Phase 1) - customers(id, external_id, first_paid_at, churned_at, segments_json) - subscriptions(id, customer_id, plan_id, started_at, ended_at, interval, amount_minor, currency, discount_json) - invoices(id, customer_id, issued_at, paid_at, refunded_at, lines_json, amount_minor, currency, fx_rate) - metric_definitions(id, name, version, rules_json, note, effective_from) · what counts, written down - movements(month, customer_id, kind, amount_minor, definition_version) · kind is new, expansion, contraction, churn or reactivation - metric_values(metric_id, definition_version, period, value_minor, components_json, computed_at) - A movement records the definition version that classified it, so a series computed under two definitions is visibly two series rather than one that jumped. ### Phase 1 · Importing billing without becoming billing Build: Import customers, subscriptions and invoices read only. Reconcile totals against the billing system's own reports. Handle a subscription changed mid-period, a refund and a failed payment that later succeeded. Trap: Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. Done when: Imported invoice totals reconcile exactly against the billing system's own report for a period. A failed payment that later succeeded does not produce a churn followed by a reactivation. A mid-period plan change is imported with both the old and new amounts. Re-importing is idempotent and changes nothing. Do not build yet: No definitions, metrics or movements. ### Phase 2 · Definitions as data Build: Store each metric's definition as explicit rules: how annual contracts are spread, how discounts are treated, how refunds are attributed, what counts as recurring. Version definitions and display the current one beside every figure. Trap: Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. Done when: Each metric has a stored rule set covering annual spreading, discounts, refunds, set-up fees and usage. The definition is displayed beside every figure rather than in documentation. Changing a definition creates a new version rather than editing the old one. Two metrics with the same name and different definitions are impossible or clearly distinguished. Do not build yet: No movements, cohorts or reporting yet. ### Phase 3 · Movements, where definitions bite Build: Classify month-over-month revenue changes per customer into new, expansion, contraction, churn and reactivation, recording the definition version used. Handle a customer who downgrades and upgrades in one month, and one who churns and returns. Trap: A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. Done when: Movements are classified with the definition version recorded per movement. A customer with both a downgrade and an upgrade in one month is handled by the stated rule and the rule is visible. Movements sum exactly to the change in recurring revenue for the period, asserted in a test. A churn followed by a return is classified by the stated reactivation window. Do not build yet: No cohorts, currency handling or reporting yet. ### Phase 4 · More than one currency Build: Store each amount in its own currency with the rate at the time of the invoice. Convert for reporting using the stored rate rather than the current one. State the rate policy beside converted figures. Trap: Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. Done when: Historic figures do not change when exchange rates move, verified by changing a rate and re-running a past period. Each amount retains its own currency and the rate applied. The rate policy is stated beside every converted figure. A customer whose currency changed mid-relationship is handled without corrupting their history. Do not build yet: No cohorts or reporting yet. ### Phase 5 · Cohorts, segments and settling arguments Build: Cohort retention and segment breakdowns computed from the same definitions. Make every figure drillable to the subscriptions and invoices behind it, and assert the detail sums to the figure. Export with definitions included. Trap: A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements. Done when: Every figure drills to its underlying subscriptions and invoices and the detail sums exactly to the figure, asserted across metrics. Cohorts and segments use the same definitions as headline figures, verified by reconciliation. Export includes the definitions used so a figure can be reproduced elsewhere. A definition change is visible as a break in the series rather than as a jump. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the billing system, which charged the money and remains authoritative. - Revenue recognition for accounting purposes, which follows standards this does not implement and should not be confused with. - Forecasting, which requires assumptions this build deliberately does not make on your behalf. - Reconciling across several billing systems, which is a data problem considerably larger than the metrics. ### README must contain - That there is no standard definition of recurring revenue, and five specific decisions change every figure - That each metric's definition is stored as data and displayed beside its figure - That definition changes create versions, so a series break is explicable rather than a jump - That exchange rates are fixed at the invoice, so historic figures do not move - That every figure drills to detail that sums to it, asserted in the suite
# Agent instructions · ChartMogul indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Read-only import from the billing system, never a second source of truth, Integer minor units with currency and the rate at the time, Every metric stored as an explicit rule set with a version, Revenue changes classified into new, expansion, contraction, churn and reactivation, Every figure openable to the subscriptions and invoices behind it. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert.
# Build plan · ChartMogul A subscription analytics layer over billing data whose difficulty is definitional rather than computational: the arithmetic is easy, the disagreement about what counts is not, and every number has to carry its own definition to be worth anything. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Importing billing without becoming billing Every charge, refund and change is imported and reconciles to the source. ### Steps 1. Import the full event history Failures and retries, not only successful charges. Files: `src/import.ts` 2. Reconcile against the source's own report Exactly. A difference is a defect rather than a rounding matter. Files: `tests/reconcile.test.ts` 3. Make import idempotent Re-running must change nothing. Files: `src/import.ts` ### Done when - [ ] Totals reconcile exactly against the billing system - [ ] A retried payment does not produce churn and reactivation - [ ] Mid-period changes import both amounts - [ ] Re-importing changes nothing ### Watch out - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. ## Phase 2 · Definitions as data Every metric has a written rule set you can read. ### Steps 1. Write the rules as data Annual spreading, discounts, refunds, fees, usage. Five decisions minimum. Files: `config/definitions.json` 2. Display the definition with the figure So a disagreement is about the rule rather than the arithmetic. Files: `src/report.ts` 3. Version rather than edit A changed definition changes history and must be visible. Files: `src/definitions.ts` ### Done when - [ ] Each metric has a stored rule set covering five decisions - [ ] Definitions display beside figures - [ ] Changes create versions rather than edits - [ ] Same-named metrics with different rules are distinguished ### Watch out - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. ## Phase 3 · Movements, where definitions bite Every change in revenue is classified consistently and traceably. ### Steps 1. Classify against the stated rules Recording which version classified each movement. Files: `src/movements.ts` 2. Assert movements sum to the change If they do not, one of the categories is wrong. Files: `tests/movement-sum.test.ts` 3. State the reactivation window A customer returning after two months is a different event from one after two years. Files: `src/movements.ts` ### Done when - [ ] Movements record the definition version applied - [ ] A downgrade and upgrade in one month follows the stated rule - [ ] Movements sum exactly to the period's change - [ ] Reactivation follows a stated window ### Watch out - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. ## Phase 4 · More than one currency Historic revenue does not change when exchange rates move. ### Steps 1. Fix the rate at the invoice Re-converting rewrites history every morning. Files: `src/currency.ts` 2. State the policy beside conversions So the figure can be reproduced by someone else. Files: `src/report.ts` 3. Handle a currency change mid-relationship It happens and it should not corrupt the customer's history. Files: `src/currency.ts` ### Done when - [ ] Historic figures are unaffected by rate changes - [ ] Each amount keeps its currency and rate - [ ] The rate policy is stated beside conversions - [ ] A mid-relationship currency change is handled ### Watch out - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. ## Not in this build - Being the billing system. - Accounting revenue recognition. - Forecasting. - Reconciling across several billing systems. ## After v1, if you want it - Segment comparison with the same definitions applied, which is where the questions go next - A definition change preview showing what a series would look like under a new rule - Customer-level revenue history, which is what account managers actually ask for
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Imported billing history for a real business. DATABASE_URL=postgres://localhost:5432/metrics # Required · secret. Read only. This never writes to the billing system. BILLING_API_KEY=a read-only key # Required. The five decisions, versioned, displayed beside every figure. DEFINITIONS_PATH=./config/definitions.json # Required. For conversion. Source amounts keep their own currency and rate. REPORTING_CURRENCY=USD # Required. Present as a constant. Re-converting rewrites history every morning. FX_POLICY=rate_at_invoice # Required. A returning customer within this is reactivation rather than new. REACTIVATION_WINDOW_DAYS=90
# ChartMogul · product brief ## Problem The visible subscription analytics loop is buildable, but a credible replacement needs more than the first screen. ChartMogul earns its keep through ingestion, storage, reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome Subscription metrics where every number states how it was defined, historic figures never move, and a disagreement about churn is a conversation about a written rule rather than about arithmetic. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - deploy target - ClickHouse or SQLite for low volume - first-party tracking script ## Explicit non-goals for v1 - Being the billing system. - Accounting revenue recognition. - Forecasting. - Reconciling across several billing systems. - session replay privacy tooling - enterprise governance and integrations - high-volume ingestion and retention - bot and identity resolution ## Success criteria - Imported totals are confirmed to reconcile against the billing system - A retried payment is confirmed not to produce churn and reactivation - Movements are confirmed to sum to the period change - Historic figures are confirmed unaffected by rate changes - Every figure is confirmed to drill to reconciling detail
# Build brief · ChartMogul The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me subscription revenue metrics where every figure states how it was defined to replace ChartMogul. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero There is no standard definition of recurring revenue, and that is the entire difficulty. An annual contract paid upfront can be counted as a twelfth per month or as revenue in the month it was paid. A discount can reduce recurring revenue or be reported separately. A refund can reverse the month it belonged to or the month it was issued. A trial converting mid-month can count fully, proportionally, or from the following month. Set-up fees and usage charges may or may not be recurring at all. Every one of those is defensible and they produce materially different figures from the same billing data. Two tools will disagree, and so will you and your investor, and the argument is unresolvable unless the definitions are written down. So each metric here stores its definition as data, displays it beside the figure, and records when a definition changed so a series that shifts can be explained by the change rather than mistaken for the business moving. ### Stack (fixed, do not substitute) - Source: Read-only import from the billing system, never a second source of truth · the billing system charged the money and is authoritative - Amounts: Integer minor units with currency and the rate at the time · converting historic revenue at today's rate rewrites history - Definitions: Every metric stored as an explicit rule set with a version · the arithmetic is trivial and the definitions are the product - Movements: Revenue changes classified into new, expansion, contraction, churn and reactivation · the categories are where definitions bite hardest - Traceability: Every figure openable to the subscriptions and invoices behind it · the only way to settle a disagreement ### Data model (create this before Phase 1) - customers(id, external_id, first_paid_at, churned_at, segments_json) - subscriptions(id, customer_id, plan_id, started_at, ended_at, interval, amount_minor, currency, discount_json) - invoices(id, customer_id, issued_at, paid_at, refunded_at, lines_json, amount_minor, currency, fx_rate) - metric_definitions(id, name, version, rules_json, note, effective_from) · what counts, written down - movements(month, customer_id, kind, amount_minor, definition_version) · kind is new, expansion, contraction, churn or reactivation - metric_values(metric_id, definition_version, period, value_minor, components_json, computed_at) - A movement records the definition version that classified it, so a series computed under two definitions is visibly two series rather than one that jumped. ### Phase 1 · Importing billing without becoming billing Build: Import customers, subscriptions and invoices read only. Reconcile totals against the billing system's own reports. Handle a subscription changed mid-period, a refund and a failed payment that later succeeded. Trap: Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. Done when: Imported invoice totals reconcile exactly against the billing system's own report for a period. A failed payment that later succeeded does not produce a churn followed by a reactivation. A mid-period plan change is imported with both the old and new amounts. Re-importing is idempotent and changes nothing. Do not build yet: No definitions, metrics or movements. ### Phase 2 · Definitions as data Build: Store each metric's definition as explicit rules: how annual contracts are spread, how discounts are treated, how refunds are attributed, what counts as recurring. Version definitions and display the current one beside every figure. Trap: Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. Done when: Each metric has a stored rule set covering annual spreading, discounts, refunds, set-up fees and usage. The definition is displayed beside every figure rather than in documentation. Changing a definition creates a new version rather than editing the old one. Two metrics with the same name and different definitions are impossible or clearly distinguished. Do not build yet: No movements, cohorts or reporting yet. ### Phase 3 · Movements, where definitions bite Build: Classify month-over-month revenue changes per customer into new, expansion, contraction, churn and reactivation, recording the definition version used. Handle a customer who downgrades and upgrades in one month, and one who churns and returns. Trap: A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. Done when: Movements are classified with the definition version recorded per movement. A customer with both a downgrade and an upgrade in one month is handled by the stated rule and the rule is visible. Movements sum exactly to the change in recurring revenue for the period, asserted in a test. A churn followed by a return is classified by the stated reactivation window. Do not build yet: No cohorts, currency handling or reporting yet. ### Phase 4 · More than one currency Build: Store each amount in its own currency with the rate at the time of the invoice. Convert for reporting using the stored rate rather than the current one. State the rate policy beside converted figures. Trap: Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. Done when: Historic figures do not change when exchange rates move, verified by changing a rate and re-running a past period. Each amount retains its own currency and the rate applied. The rate policy is stated beside every converted figure. A customer whose currency changed mid-relationship is handled without corrupting their history. Do not build yet: No cohorts or reporting yet. ### Phase 5 · Cohorts, segments and settling arguments Build: Cohort retention and segment breakdowns computed from the same definitions. Make every figure drillable to the subscriptions and invoices behind it, and assert the detail sums to the figure. Export with definitions included. Trap: A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements. Done when: Every figure drills to its underlying subscriptions and invoices and the detail sums exactly to the figure, asserted across metrics. Cohorts and segments use the same definitions as headline figures, verified by reconciliation. Export includes the definitions used so a figure can be reproduced elsewhere. A definition change is visible as a break in the series rather than as a jump. Do not build yet: Nothing after this. ### Out of scope (and why) - Being the billing system, which charged the money and remains authoritative. - Revenue recognition for accounting purposes, which follows standards this does not implement and should not be confused with. - Forecasting, which requires assumptions this build deliberately does not make on your behalf. - Reconciling across several billing systems, which is a data problem considerably larger than the metrics. ### README must contain - That there is no standard definition of recurring revenue, and five specific decisions change every figure - That each metric's definition is stored as data and displayed beside its figure - That definition changes create versions, so a series break is explicable rather than a jump - That exchange rates are fixed at the invoice, so historic figures do not move - That every figure drills to detail that sums to it, asserted in the suite
# Architecture · ChartMogul ## Stack | Part | Choice | Why | | --- | --- | --- | | Source | Read-only import from the billing system, never a second source of truth | the billing system charged the money and is authoritative | | Amounts | Integer minor units with currency and the rate at the time | converting historic revenue at today's rate rewrites history | | Definitions | Every metric stored as an explicit rule set with a version | the arithmetic is trivial and the definitions are the product | | Movements | Revenue changes classified into new, expansion, contraction, churn and reactivation | the categories are where definitions bite hardest | | Traceability | Every figure openable to the subscriptions and invoices behind it | the only way to settle a disagreement | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Full event history reconciled to the source. | Any billing system, provided failures and retries are imported rather than only successes. | | Definitions | Versioned rule sets displayed with figures. | Not swappable for definitions in code, which nobody can read when asked. | | Movements | Classification with the version recorded. | Any classifier, provided movements sum exactly to the period change. | | Currency | Rates fixed at the invoice. | Any source, provided historic figures never move. | | Traceability | Drilling that reconciles. | Not swappable. A figure nobody can trace is a figure nobody can defend. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Imported billing history for a real business. - `BILLING_API_KEY` · required, secret · Read only. This never writes to the billing system. - `DEFINITIONS_PATH` · required · The five decisions, versioned, displayed beside every figure. - `REPORTING_CURRENCY` · required · For conversion. Source amounts keep their own currency and rate. - `FX_POLICY` · required · Present as a constant. Re-converting rewrites history every morning. - `REACTIVATION_WINDOW_DAYS` · required · A returning customer within this is reactivation rather than new. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · ChartMogul product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Read-only import from the billing system, never a second source of truth, Integer minor units with currency and the rate at the time, Every metric stored as an explicit rule set with a version, Revenue changes classified into new, expansion, contraction, churn and reactivation, Every figure openable to the subscriptions and invoices behind it. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. - A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements.
# Delivery milestones · ChartMogul Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Importing billing without becoming billing Every charge, refund and change is imported and reconciles to the source. ### Steps 1. Import the full event history Failures and retries, not only successful charges. Files: `src/import.ts` 2. Reconcile against the source's own report Exactly. A difference is a defect rather than a rounding matter. Files: `tests/reconcile.test.ts` 3. Make import idempotent Re-running must change nothing. Files: `src/import.ts` ### Done when - [ ] Totals reconcile exactly against the billing system - [ ] A retried payment does not produce churn and reactivation - [ ] Mid-period changes import both amounts - [ ] Re-importing changes nothing ### Watch out - Importing only successful invoices misses the failed payment that succeeded three days later, so a customer appears churned and then reactivated in the same month, which corrupts every movement figure. Import the full event history including failures and retries, and derive state from it. ## M2 · Definitions as data Every metric has a written rule set you can read. ### Steps 1. Write the rules as data Annual spreading, discounts, refunds, fees, usage. Five decisions minimum. Files: `config/definitions.json` 2. Display the definition with the figure So a disagreement is about the rule rather than the arithmetic. Files: `src/report.ts` 3. Version rather than edit A changed definition changes history and must be visible. Files: `src/definitions.ts` ### Done when - [ ] Each metric has a stored rule set covering five decisions - [ ] Definitions display beside figures - [ ] Changes create versions rather than edits - [ ] Same-named metrics with different rules are distinguished ### Watch out - Encoding the definitions in code means the answer to what counts is spread across functions nobody reads, and when an investor asks how you compute recurring revenue the honest answer is that you would have to go and look. Store the rules as data, print them beside the number, and version them. ## M3 · Movements, where definitions bite Every change in revenue is classified consistently and traceably. ### Steps 1. Classify against the stated rules Recording which version classified each movement. Files: `src/movements.ts` 2. Assert movements sum to the change If they do not, one of the categories is wrong. Files: `tests/movement-sum.test.ts` 3. State the reactivation window A customer returning after two months is a different event from one after two years. Files: `src/movements.ts` ### Done when - [ ] Movements record the definition version applied - [ ] A downgrade and upgrade in one month follows the stated rule - [ ] Movements sum exactly to the period's change - [ ] Reactivation follows a stated window ### Watch out - A customer who downgrades on the first of the month and upgrades on the twentieth can be recorded as contraction then expansion, as a single net change, or as neither, and the three produce very different churn figures from identical facts. Decide the rule, record which was applied, and make it visible per movement. ## M4 · More than one currency Historic revenue does not change when exchange rates move. ### Steps 1. Fix the rate at the invoice Re-converting rewrites history every morning. Files: `src/currency.ts` 2. State the policy beside conversions So the figure can be reproduced by someone else. Files: `src/report.ts` 3. Handle a currency change mid-relationship It happens and it should not corrupt the customer's history. Files: `src/currency.ts` ### Done when - [ ] Historic figures are unaffected by rate changes - [ ] Each amount keeps its currency and rate - [ ] The rate policy is stated beside conversions - [ ] A mid-relationship currency change is handled ### Watch out - Converting all historic revenue at today's rate means last year's figures change every day, so a report run this morning disagrees with the one you sent an investor last week and neither is wrong. Fix the rate at the invoice and never re-convert. ## M5 · Cohorts, segments and settling arguments (production only) Every figure opens to the customers and invoices behind it. ### Steps 1. Compute everything from one definition set Two definitions produce two authoritative numbers that disagree. Files: `src/cohorts.ts` 2. Assert drilled detail sums to every figure Across every metric, in the suite. Files: `tests/drill-sums.test.ts` 3. Export the definitions with the data So the figures can be reproduced rather than trusted. Files: `src/export.ts` ### Done when - [ ] Every figure drills to reconciling detail - [ ] Cohorts use the same definitions as headline figures - [ ] Export includes the definitions used - [ ] Definition changes appear as series breaks ### Watch out - A cohort chart computed with a slightly different definition from the headline metric produces two numbers that both look authoritative and disagree, and nobody can work out why. Compute everything from the same stored definitions, and assert in the suite that a cohort's totals reconcile to the corresponding movements.
# Operations · ChartMogul ## Backup Billing data re-imports from the source, but definition versions and their change history do not, since they record decisions rather than data. Back those up first. ## Restore Restore and confirm definition versions and their effective dates survived, since recomputing under current definitions would silently rewrite historic figures that were already reported. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Reconciliation against the billing system on every import is the health check. A drift means the import is wrong, and every figure after that point is plausible and incorrect. ## Incident checklist A figure quoted externally that no longer reproduces is the failure that matters, and it almost always means either a definition changed without a version or rates were re-applied. Both are visible in the stored versions. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Imported totals are confirmed to reconcile against the billing system - [ ] A retried payment is confirmed not to produce churn and reactivation - [ ] Movements are confirmed to sum to the period change - [ ] Historic figures are confirmed unaffected by rate changes - [ ] Every figure is confirmed to drill to reconciling detail ## Launch constraint Do not market omitted ChartMogul capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Imported billing history for a real business. DATABASE_URL=postgres://localhost:5432/metrics # Required · secret. Read only. This never writes to the billing system. BILLING_API_KEY=a read-only key # Required. The five decisions, versioned, displayed beside every figure. DEFINITIONS_PATH=./config/definitions.json # Required. For conversion. Source amounts keep their own currency and rate. REPORTING_CURRENCY=USD # Required. Present as a constant. Re-converting rewrites history every morning. FX_POLICY=rate_at_invoice # Required. A returning customer within this is reactivation rather than new. REACTIVATION_WINDOW_DAYS=90
$ choose a build depth, inspect the files, then open the complete pack in your agent
ChartMogul: Customers pay for trusted numbers, retention, privacy controls, and a pipeline that remains accurate while traffic and schemas change.
xsession replay privacy tooling
xenterprise governance and integrations
xhigh-volume ingestion and retention
xbot and identity resolution
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
ChartMogul pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/workspace | $0/workspace | Free while MRR is at or below $10,000; core revenue metrics and workflows |
| starter | $69/workspace | $59/workspace | Starts above $10,000 MRR; up to 3 team members and 1 billing system |
| pro | $119/workspace | $99/workspace | Unlimited team members and multiple billing systems |
free tierFree while the business is at or below $10,000 MRR; no published seat cap on Free
billingmonthly + annual (17% off); Starter and Pro prices scale with company ARR
hidden costsThe paid price rises automatically with ARR; Starter is capped at 3 team members and 1 billing system, so additional users or multiple billing systems require Pro.
verified 2026-08-12 · source ↗
Vibecode ChartMogul
Kinda. The core of ChartMogul is buildable in a weekend with the prompt on this page, but there are real gaps: session replay privacy tooling, enterprise governance and integrations. Read the honest list above before committing.
How much does ChartMogul cost?
ChartMogul's pricing is usage-based or varies by plan · The pricing model is not safely comparable as one fixed monthly USD amount. Review the official source before publication..
What do I lose by replacing ChartMogul?
Honestly: session replay privacy tooling; enterprise governance and integrations; high-volume ingestion and retention; bot and identity resolution. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to ChartMogul?
Yes: ProfitWell Metrics (MRR, churn, cohorts and segmentation for zero dollars; advanced forecasting is still somebody else's job.) The prompt is for when you want it exactly your way.