Creator Analytics — Technical Reference
The dashboard is mostly a composition of analytics that already existed on the backend (earnings summary, earnings-by-source, exclusive-post earnings, subscriber count), plus several genuinely new backend queries added across passes: a daily earnings time series (Fase D.4 / roadmap 4.12), monthly subscriber retention/churn, and a calendar-year earnings statement. The page it replaces was an honest stub that declared creator analytics had "no backend support"; that was already out of date.
Where this lives
Backend
apps/backend/graphql/types/coin-transaction.type.js— newEarningsTimePointtype andmyEarningsTimeSeries(days: Int): [EarningsTimePoint!]!query, alongside the pre-existingmyEarningsBySource.apps/backend/graphql/resolvers/coin-transaction.resolver.js—myEarningsTimeSeriesresolver (authenticated).apps/backend/managers/coin-managers/coin-transaction.manager.js—getEarningsTimeSeries(userId, days=30): clamps the window to ≤365 days and zero-fills every day in the range so the frontend charts a continuous series rather than only days with sales.apps/backend/data-access-services/coin/coin-transaction.access-service.js—getEarningsTimeSeries(userId, since): a PostgresGROUP BY DATE(created_at)SUM(amount)aggregate, scoped to genuine earning transaction types (tip_received,post_sale,subscription_revenue,product_sale, plus paid-messagereward/message_purchase) rather than a bareamount > 0— so a creator's own coin purchases don't inflate the trend, matching howgetEarningsBySourcedefines earnings.apps/backend/graphql/types/user-subscription.type.js—SubscriberRetentionPointtype andsubscriberRetention(months: Int): [SubscriberRetentionPoint!]!query.apps/backend/graphql/resolvers/user-subscription.resolver.js—subscriberRetentionresolver (authenticated; swallows manager errors and returns[]).apps/backend/managers/payment-managers/user-subscription.manager.js—getSubscriberRetention(creatorId, { months }, context): clampsmonthsto 1–24 (default 6), buckets the creator's fullUserSubscriptionhistory by calendar month in application code, and computes new / churned / active-at-end counts plus churn and retention rates per month.apps/backend/graphql/types/coin-transaction.type.js—EarningsStatementtype andmyEarningsStatement(year: Int): EarningsStatement!query (also documented under Payouts, since it's built alongside cashout/payout logic).apps/backend/managers/coin-managers/coin-transaction.manager.js—getEarningsStatement(userId, year, context): calendar-year earnings-by-source breakdown plustotalCashedOut; a self-serve summary, not an official tax form.- Pre-existing, reused as-is:
myEarningsSummary(coin-payout.type.js),myEarningsBySource(coin-transaction.type.js),myPostPurchaseEarnings/myPostPurchaseSales(post-purchase.type.js),subscriberCount(user-subscription.type.js).
Frontend
apps/frontend-nextjs/src/page-components/settings/InsightsAndToolsPage.tsx(routed at/settings/insights-tools) — the dashboard. Was an honest stub; now composes the cards below, queriesmyPostPurchaseEarnings,subscriberCount, andsubscriberRetention(months: 6), and has a client-side "Exportar CSV" export (handleExportCsv) that builds a CSV from the earnings-summary numbers and the 6-month retention table already in memory — no dedicated export query.apps/frontend-nextjs/src/page-components/settings/CreatorSalesPage.tsx(routed at/settings/sales, "Sales & earnings") — sales/purchases tabs overmyPostPurchaseSales/myPurchasedPosts, plus a client-computed "Top buyers" list (byBuyer) that groups the current sales page bybuyer.idwith per-buyer sale counts andsellerEarningsCoinstotals.apps/frontend-nextjs/src/components/payouts/EarningsTrendChart.tsx— the daily-trend bar chart (recharts), queryingmyEarningsTimeSerieswith a 7d/30d/90d range switch.apps/frontend-nextjs/src/components/payouts/EarningsSummaryCard.tsx,EarningsBySourceChart.tsx, andEarningsStatementCard.tsx— pre-existing, reused unchanged (EarningsStatementCardqueriesmyEarningsStatementfor a downloadable per-year statement and is otherwise documented under Payouts). The first two used to also render on/settings/get-coins(the coin-purchase page) — removed from there: it duplicated this dashboard, and worse, buying your own coins visibly bumped "lifetime earned" on that card (see thelifetimeEarnedfix note under Payouts).
Technical implementation checklist
-
myEarningsTimeSeries(days)— query/resolver/manager/access-service, zero-filled daily series;EarningsTrendChart.tsx - Exclusive post sales — surfaces
myPostPurchaseEarnings(backend already existed; no frontend consumer before) - Active subscriber count — surfaces
subscriberCount(creatorId) - Reused earnings summary + by-source cards
- Subscriber retention time-series —
subscriberRetention(months)query/resolver/manager, 6-month bar chart + churn onInsightsAndToolsPage.tsx - Per-buyer sales view —
myPostPurchaseSalessurfaced via a client-grouped "Top buyers" list onCreatorSalesPage.tsx(/settings/sales) - CSV export — client-side only (
handleExportCsvinInsightsAndToolsPage.tsx); no dedicated export query - Annual earnings statement —
myEarningsStatement(year)viaEarningsStatementCard.tsx, also rendered on this page (primary docs under Payouts)
GraphQL API
# Daily earnings trend, zero-filled, last N days (default 30, max 365)
query MyEarningsTimeSeries($days: Int) {
myEarningsTimeSeries(days: $days) {
date # YYYY-MM-DD
earnings # coins earned that day
}
}
# Monthly subscriber retention/churn, oldest -> newest (default 6 months, max 24)
query SubscriberRetention($months: Int) {
subscriberRetention(months: $months) {
month # YYYY-MM
newSubscribers
churned
activeAtEnd
churnRate # percent
retentionRate # percent
}
}
# Reused on the dashboard
query DashboardExtras($creatorId: ID!) {
myEarningsSummary { availableForCashout unmaturedRecentEarnings }
myEarningsBySource(days: 30) { tips contentSales profileSubscriptions groupSubscriptions marketplace other }
myPostPurchaseEarnings { totalSales totalRevenue totalEarnings platformFees }
myPostPurchaseSales(limit: 20, offset: 0) { id coinPrice sellerEarningsCoins status createdAt buyer { id username } }
subscriberCount(creatorId: $creatorId)
myEarningsStatement(year: 2026) { totalEarnings tips contentSales profileSubscriptions groupSubscriptions marketplace other totalCashedOut }
}
Notes
- Earnings definition is shared. Both
myEarningsBySourceandmyEarningsTimeSeriescount the same transaction types as "earnings," so the trend total and the by-source total agree for a given window. Coin purchases and admin-granted bonuses are excluded from the trend (they'd otherwise show up as spurious spikes). - Zero-filling happens in the manager, not SQL — the access-service returns only days with earnings, and
getEarningsTimeSeriesin the manager expands that into a continuous day-by-day array. This keeps the query cheap while giving the chart a clean axis. - Retention buckets are computed in application code, not SQL —
getSubscriberRetentionpulls the creator's fullUserSubscriptionhistory once (getAllByCreatorForAnalytics) and buckets it into calendar months in JS. "Active at end" for a month counts subscriptions created before the bucket's end and not cancelled before it; churn rate is churned-in-month over active-at-start. - CSV/statement exports are client-side. The "Exportar CSV" button on Insights & Tools and the statement download on
EarningsStatementCardboth build their file (CSV / plain text) from already-fetched query data in the browser — there's no server-side export endpoint or file storage involved.