Top customers
The accounts paying the most, with their plan, owner, MRR and change on the month, and what share of the book they are together; reads topCustomers().
Preview
import { formatPercent } from "@/lib/format"
import { SimpleTable, type SimpleTableColumn } from "@/components/ui/simple-table"
import { CurrencyCell, DeltaCell } from "@/components/ui/table-cells"
import { Widget } from "@/components/ui/widget"
import { topCustomers, topCustomersShare, type TopCustomer } from "../data"
const COLUMNS: SimpleTableColumn<TopCustomer>[] = [
{ key: "company", header: "Account", cell: (row) => <span className="font-medium">{row.company}</span> },
{ key: "plan", header: "Plan", cell: (row) => <span className="text-muted-foreground">{row.plan}</span> },
{ key: "owner", header: "Owner", cell: (row) => <span className="text-muted-foreground">{row.owner}</span> },
{ key: "mrr", header: "MRR", align: "right", cell: (row) => <CurrencyCell value={row.mrr} /> },
{
key: "delta",
header: "On the month",
align: "right",
cell: (row) => <DeltaCell value={row.delta} />,
},
]
export function TopCustomers() {
const rows = topCustomers()
return (
<Widget
data-widget="widget-saas-revenue-top-customers"
title="Top customers"
description={`Top ${rows.length} accounts by MRR`}
className="h-full"
footer={`Together they are ${formatPercent(topCustomersShare(rows), { maximumFractionDigits: 0 })} of the book.`}
>
<SimpleTable size="sm" columns={COLUMNS} rows={rows} rowKey="company" hoverable />
</Widget>
)
}Install
$
npx shadcn@latest add @vibra/widget-saas-revenue-top-customersNeeds the @vibra registry in your components.json — set it up once.
Source
import { formatPercent } from "@/lib/format"
import { SimpleTable, type SimpleTableColumn } from "@/components/ui/simple-table"
import { CurrencyCell, DeltaCell } from "@/components/ui/table-cells"
import { Widget } from "@/components/ui/widget"
import { topCustomers, topCustomersShare, type TopCustomer } from "../data"
const COLUMNS: SimpleTableColumn<TopCustomer>[] = [
{ key: "company", header: "Account", cell: (row) => <span className="font-medium">{row.company}</span> },
{ key: "plan", header: "Plan", cell: (row) => <span className="text-muted-foreground">{row.plan}</span> },
{ key: "owner", header: "Owner", cell: (row) => <span className="text-muted-foreground">{row.owner}</span> },
{ key: "mrr", header: "MRR", align: "right", cell: (row) => <CurrencyCell value={row.mrr} /> },
{
key: "delta",
header: "On the month",
align: "right",
cell: (row) => <DeltaCell value={row.delta} />,
},
]
export function TopCustomers() {
const rows = topCustomers()
return (
<Widget
data-widget="widget-saas-revenue-top-customers"
title="Top customers"
description={`Top ${rows.length} accounts by MRR`}
className="h-full"
footer={`Together they are ${formatPercent(topCustomersShare(rows), { maximumFractionDigits: 0 })} of the book.`}
>
<SimpleTable size="sm" columns={COLUMNS} rows={rows} rowKey="company" hoverable />
</Widget>
)
}/**
* The book: how a subscription becomes revenue, and what the revenue was on any
* given date. `db.subscriptions` records one rate per account and the two dates
* it ran between, so a month's book is every subscription that had started by
* the close and had not been cancelled yet.
*
* `data.ts` holds the selectors the page reads; this is the model underneath
* them, kept apart because it is the part a consumer swaps when the rows come
* from a real billing system.
*/
import { db, REFERENCE_DATE, type Customer, type Subscription } from "@/lib/sample-data"
/** The plan tiers that carry revenue, largest first — Free never enters the book. */
export type PaidTier = Exclude<Customer["plan"], "free">
export const PAID_TIERS = ["enterprise", "team", "starter"] as const satisfies readonly PaidTier[]
/**
* Every subscription, read per call and never held at module scope: an account
* cancelled or started since the server started is in the next book.
*/
export function subscriptions(): Subscription[] {
return db.subscriptions.all()
}
export const PLAN_NAME_BY_ID = new Map(db.plans.all().map((plan) => [plan.id, plan.name]))
const PLAN_BY_TIER = new Map(db.plans.all().map((plan) => [plan.name.toLowerCase(), plan]))
/** The plan a tier names. Customer.plan and Plan.name are the same four tiers. */
export function planFor(tier: PaidTier) {
return PLAN_BY_TIER.get(tier)!
}
// What a plan charges a seat a month, read off the accounts paying it today:
// every live account on a tier bills the same rate, so one of them states it
// for the accounts that have since stopped paying.
export function seatPrices(): Map<string, number> {
const prices = new Map<string, number>()
for (const customer of db.customers.all()) {
if (customer.mrrCents > 0 && !prices.has(customer.plan)) {
prices.set(customer.plan, customer.mrrCents / customer.seats)
}
}
return prices
}
/** What a subscription bills a month, in cents. A trial bills nothing. */
export function billedCents(subscription: Subscription, prices = seatPrices()): number {
if (subscription.status === "trialing") return 0
const tier = (PLAN_NAME_BY_ID.get(subscription.planId) ?? "").toLowerCase()
return subscription.seats * (prices.get(tier) ?? 0)
}
/** Midnight UTC on the first of the month `k` months before the current one. */
export function monthStart(k: number): Date {
return new Date(Date.UTC(REFERENCE_DATE.getUTCFullYear(), REFERENCE_DATE.getUTCMonth() - k, 1))
}
/** The month that ends at `monthStart(k)`, as "2026-08". */
export function closedMonthKey(k: number): string {
return monthStart(k + 1).toISOString().slice(0, 7)
}
/** Whether a subscription was billing at `at`. */
export const live = (subscription: Subscription, at: Date): boolean =>
subscription.startedAt <= at && (!subscription.cancelledAt || subscription.cancelledAt > at)
/** The book at `at`, in dollars, for one plan or for all of them. */
export function bookAt(at: Date, planId?: string): number {
const prices = seatPrices()
return (
subscriptions()
.filter((row) => live(row, at) && (!planId || row.planId === planId))
.reduce((total, row) => total + billedCents(row, prices), 0) / 100
)
}
/** What started billing, and what stopped, between two instants — in dollars. */
export function movement(from: Date, to: Date): { won: number; lost: number } {
let won = 0
let lost = 0
const prices = seatPrices()
for (const subscription of subscriptions()) {
if (subscription.startedAt > from && subscription.startedAt <= to) won += billedCents(subscription, prices)
const { cancelledAt } = subscription
if (cancelledAt && cancelledAt > from && cancelledAt <= to) lost += billedCents(subscription, prices)
}
return { won: won / 100, lost: lost / 100 }
}/**
* What this page reads. The book is `db.subscriptions` walked month by month by
* `./book`, so the twelve-month chart, the closing MRR, the movements, the
* ratios and the quarter tracker all come off the same rows. The page closes
* each month rather than reading a part-finished one; "now" is `REFERENCE_DATE`.
*/
import { formatCurrency, formatPercent, getInitials } from "@/lib/format"
import { db, REFERENCE_DATE, seeded, type Member, type Subscription } from "@/lib/sample-data"
import {
billedCents,
bookAt,
closedMonthKey,
live,
monthStart,
movement,
PAID_TIERS,
planFor,
PLAN_NAME_BY_ID,
seatPrices,
subscriptions,
type PaidTier,
} from "./book"
const MONTHS = 12
/** One month of the book. The keys are the paid tiers, so a typo will not compile. */
export type MonthlyRevenue = { month: string } & Record<PaidTier, number>
/** Twelve closed months of MRR split by plan, oldest first. Each row is that month's book. */
export function revenueByPlan(): MonthlyRevenue[] {
return Array.from({ length: MONTHS }, (_, index) => {
const k = MONTHS - 1 - index
const at = monthStart(k)
return {
month: closedMonthKey(k),
enterprise: Math.round(bookAt(at, planFor("enterprise").id)),
team: Math.round(bookAt(at, planFor("team").id)),
starter: Math.round(bookAt(at, planFor("starter").id)),
}
})
}
/** A tier a revenue chart stacks: the key its rows carry, and the plan's name. */
export type PlanKey = { key: PaidTier; label: string }
/** The tiers a revenue chart stacks, largest first. */
export function planKeys(): PlanKey[] {
return PAID_TIERS.map((tier) => ({ key: tier, label: planFor(tier).name }))
}
const CLOSE_AT = monthStart(0)
const OPENING_AT = monthStart(1)
const YEAR_AGO_AT = monthStart(MONTHS)
/** Monthly recurring revenue at the close of the last complete month, read off the book as it stands. */
const closingMrr = () => Math.round(bookAt(CLOSE_AT))
/** The closing split by plan, in the shape PercentageBar takes. */
export function revenueMix(): { label: string; value: number }[] {
return PAID_TIERS.map((tier) => ({
label: planFor(tier).name,
value: Math.round(bookAt(CLOSE_AT, planFor(tier).id)),
}))
}
/** Enterprise's share of the book now, and twelve months ago. */
export function enterpriseShare(): { now: number; yearAgo: number } {
const enterprise = planFor("enterprise").id
return {
now: bookAt(CLOSE_AT, enterprise) / bookAt(CLOSE_AT),
yearAgo: bookAt(YEAR_AGO_AT, enterprise) / bookAt(YEAR_AGO_AT),
}
}
/**
* What the book won and lost between two month closes, over the same twelve
* months the chart above draws — a single month of this data is mostly trials
* that have not started paying. Two movements rather than the usual four: an
* account moving up or down a plan leaves no trace in the rows, and a made-up
* expansion would need a made-up contraction to cancel it.
*/
function movements(fromK: number, toK: number) {
const opening = bookAt(monthStart(fromK))
const { won, lost } = movement(monthStart(fromK), monthStart(toK))
return { won, lost, opening, net: bookAt(monthStart(toK)) - opening }
}
/** The book the twelve months of movement below opened against. */
export function mrrYearOpening(): number {
return Math.round(movements(MONTHS, 0).opening)
}
/** One step of the year's MRR walk, as the waterfall draws it. */
export type MrrMovement = { label: string; value: number; isTotal?: boolean }
/** How the closing book got to where it is over the twelve months behind it. */
export function mrrMovements(): MrrMovement[] {
const YEAR = movements(MONTHS, 0)
return [
{ label: "New", value: Math.round(YEAR.won) },
{ label: "Churn", value: -Math.round(YEAR.lost) },
{ label: "Net new", value: 0, isTotal: true },
]
}
const money = (value: number) => formatCurrency(value, "USD", { maximumFractionDigits: 0 })
const paying = (at: Date): number => {
const prices = seatPrices()
return subscriptions().filter((subscription) => live(subscription, at) && billedCents(subscription, prices) > 0)
.length
}
const retained = (span: ReturnType<typeof movements>): number => (span.opening - span.lost) / span.opening
/** One of the four ratios beside the headline number: printed, and moved against the year before. */
export type HeadlineStat = { label: string; value: string; delta: number; positiveIsGood?: boolean }
/** The four ratios beside the headline number, each against the year before it. */
export function headlineStats(): HeadlineStat[] {
const YEAR = movements(MONTHS, 0)
const YEAR_BEFORE = movements(MONTHS * 2, MONTHS)
const arpu = closingMrr() / paying(CLOSE_AT)
const wasArpu = bookAt(YEAR_AGO_AT) / paying(YEAR_AGO_AT)
const churn = YEAR.lost / YEAR.opening
const wasChurn = YEAR_BEFORE.lost / YEAR_BEFORE.opening
const kept = retained(YEAR)
// What an account is worth at a churn rate: the months it lasts, times what
// it pays for each of them.
const months = (rate: number) => (rate > 0 ? 12 / rate : 36)
const ltv = arpu * months(churn)
const wasLtv = wasArpu * months(wasChurn)
return [
{ label: "ARPU", value: money(arpu), delta: arpu / wasArpu - 1 },
{
label: "Gross churn",
// A fall is the win, so the delta is read the other way up.
value: formatPercent(churn, { maximumFractionDigits: 1 }),
delta: wasChurn > 0 ? churn / wasChurn - 1 : 0,
positiveIsGood: false,
},
{ label: "Lifetime value", value: money(ltv), delta: ltv / wasLtv - 1 },
{
label: "Gross retention",
value: formatPercent(kept, { maximumFractionDigits: 0 }),
delta: kept / retained(YEAR_BEFORE) - 1,
},
]
}
export type RevenueWindow = {
key: "1m" | "3m" | "12m"
/** How the window reads in a sentence, e.g. "last quarter". */
label: string
won: number
lost: number
net: number
/** Net new as a share of what the book opened the window at. */
growth: number
/** Each against the same length of book immediately before it. */
wonDelta: number
lostDelta: number
netDelta: number
growthDelta: number
}
// The three windows a revenue conversation actually compares. Month counts,
// not dates: the book is walked month by month, and a part-finished month is
// mostly trials that have not started paying.
const WINDOWS = [
{ key: "1m", label: "last month", months: 1 },
{ key: "3m", label: "last quarter", months: 3 },
{ key: "12m", label: "last year", months: MONTHS },
] as const
/**
* What the book won and lost over each window, and how that compares with the
* same length of book immediately before it — the same two movements the
* waterfall draws, read at three lengths so the page can be asked a different
* question without a round trip.
*/
export function revenueWindows(): RevenueWindow[] {
const ratio = (now: number, before: number) => (before > 0 ? now / before - 1 : 0)
return WINDOWS.map(({ key, label, months }) => {
const span = movements(months, 0)
const before = movements(months * 2, months)
const growth = span.opening > 0 ? span.net / span.opening : 0
const wasGrowth = before.opening > 0 ? before.net / before.opening : 0
return {
key,
label,
won: Math.round(span.won),
lost: Math.round(span.lost),
net: Math.round(span.net),
growth,
wonDelta: ratio(span.won, before.won),
lostDelta: ratio(span.lost, before.lost),
netDelta: ratio(span.net, before.net),
growthDelta: ratio(growth, wasGrowth),
}
})
}
const DAY_MS = 86_400_000
const MONTH_NAME = new Intl.DateTimeFormat("en-US", { month: "long", timeZone: "UTC" })
// The quarter runs to the end of its third month, not to the start of it.
const QUARTER_END = new Date(
Date.UTC(REFERENCE_DATE.getUTCFullYear(), Math.floor(REFERENCE_DATE.getUTCMonth() / 3) * 3 + 3, 1) - 1
)
/** Everything the headline card prints: the closing book, how it moved, who pays it, and the four ratios. */
export type RevenueHeadline = {
mrr: number
/** On the month before. */
delta: number
/** On the same close a year earlier. */
yoy: number
customers: number
stats: HeadlineStat[]
}
/** The headline card's figures, read here and handed to the card: the card is an island, and never reads the book. */
export function revenueHeadline(): RevenueHeadline {
const mrr = closingMrr()
return {
mrr,
delta: mrr / Math.round(bookAt(OPENING_AT)) - 1,
yoy: mrr / bookAt(YEAR_AGO_AT) - 1,
customers: db.customers.all().length,
stats: headlineStats(),
}
}
const QUARTER_LABEL = `Q${Math.floor(REFERENCE_DATE.getUTCMonth() / 3) + 1}`
// Months into the quarter so far, so the opening is the quarter's own start.
const quarterOpening = () => bookAt(monthStart(REFERENCE_DATE.getUTCMonth() % 3))
/** Whole days left in the quarter, counting the rest of today. */
export const QUARTER_DAYS_LEFT = Math.floor((QUARTER_END.getTime() - REFERENCE_DATE.getTime()) / DAY_MS)
/** Net new MRR booked this quarter, against the number the board was given. */
export function quarterGoal() {
const opening = quarterOpening()
const current = Math.round(closingMrr() - opening)
const months = REFERENCE_DATE.getUTCMonth() % 3
return {
label: `${QUARTER_LABEL} net new MRR`,
current,
// A round number a little beyond the run rate: a target, not a forecast.
goal: Math.ceil((current * 1.55) / 1_000) * 1_000,
milestones: Array.from({ length: Math.max(0, months - 1) }, (_, index) => ({
value: Math.round(bookAt(monthStart(months - 1 - index)) - opening),
label: MONTH_NAME.format(monthStart(months - index)).slice(0, 3),
})),
deadline: `Quarter closes ${QUARTER_END.getUTCDate()} ${MONTH_NAME.format(QUARTER_END)}`,
}
}
/**
* The book the quarter has to close on: what it opened at plus the number the
* board was given. The goal itself is net new, so a chart of the whole book
* plots this rather than the difference — the same target, read on the scale
* the chart is already on.
*/
export function quarterTarget(): { value: number; label: string } {
return { value: Math.round(quarterOpening() + quarterGoal().goal), label: `${QUARTER_LABEL} target` }
}
/** The month the book is read at, e.g. "August 2026". */
export function bookAsOf(): string {
return `${MONTH_NAME.format(monthStart(1))} ${monthStart(1).getUTCFullYear()}`
}
export type TopCustomer = {
company: string
plan: string
owner: string
mrr: number
/** Change in that account's MRR on the month, as a ratio. */
delta: number
}
const TOP_COUNT = 8
const YEAR_MS = 365 * 86_400_000
// Growth decays with an account's age, and the floor is what it decays past:
// the two meet at about fifteen months, which is where an account stops adding
// seats faster than it loses them.
const GROWTH_AT_NEW = 0.26
const SETTLED_DRIFT = 0.078
/**
* What an account did on the month. A subscription carries one rate rather than
* a history of them, so this is a rule over its age: an account still in its
* first year is adding seats fast, and one past its second is settled and
* drifting slightly down. A book this size holds both, so the column does too.
*/
function monthOnMonth(subscription: Subscription): number {
const years = (CLOSE_AT.getTime() - subscription.startedAt.getTime()) / YEAR_MS
return Math.round((GROWTH_AT_NEW * Math.exp(-years) - SETTLED_DRIFT) * 1000) / 1000
}
/** The largest accounts by MRR, with what each did on the month. */
export function topCustomers(): TopCustomer[] {
// Read per call, never held at module scope: an account or an owner written
// since the server started is what the next render shows.
const members = new Map(db.members.all().map((member) => [member.id, member]))
const customers = new Map(db.customers.all().map((customer) => [customer.id, customer]))
const prices = seatPrices()
return subscriptions()
.filter((subscription) => live(subscription, CLOSE_AT))
.sort((a, b) => billedCents(b, prices) - billedCents(a, prices))
.slice(0, TOP_COUNT)
.map((subscription) => {
const customer = customers.get(subscription.customerId)
return {
company: customer?.company ?? subscription.customerId,
plan: PLAN_NAME_BY_ID.get(subscription.planId) ?? "—",
owner: members.get(customer?.owner ?? "")?.name ?? "Unassigned",
mrr: billedCents(subscription, prices) / 100,
delta: monthOnMonth(subscription),
}
})
}
/** What those accounts add up to, as a share of the whole book. */
export function topCustomersShare(rows: TopCustomer[]): number {
return rows.reduce((sum, customer) => sum + customer.mrr, 0) / closingMrr()
}
/**
* How much of what customers pay for they actually use — the expansion signal.
* Seats are counted off the subscriptions against what their plans include;
* events and storage have no entity, so they come off `seeded("dashboard-02-usage")`.
*/
export function committedUsage() {
const rand = seeded("dashboard-02-usage")
// Seats bought is what the accounts pay for; how many of them are in use is
// not something a subscription records, and nor are events or storage.
const prices = seatPrices()
const seats = subscriptions()
.filter((subscription) => live(subscription, CLOSE_AT) && billedCents(subscription, prices) > 0)
.reduce((sum, subscription) => sum + subscription.seats, 0)
const used = (limit: number, floor: number, spread: number) => limit * (floor + rand() * spread)
return [
{ label: "Seats", used: Math.round(used(seats, 0.82, 0.1)), limit: seats, unit: "seats", compact: false },
{ label: "Events", used: Math.round(used(12e6, 0.62, 0.12)), limit: 12e6, unit: "events", compact: true },
{ label: "Storage", used: Math.round(used(10, 0.58, 0.14) * 10) / 10, limit: 10, unit: "TB", compact: false },
]
}
function ownerRow(): Member {
return db.members.all().find((member) => member.role === "owner") ?? db.members.all()[0]
}
/** The person looking at the page: whoever owns this workspace. */
export function currentUser() {
const owner = ownerRow()
return { name: owner.name, email: owner.email, initials: getInitials(owner.name), avatarUrl: owner.avatarUrl }
}
/** The bell's contents: the newest notifications, unread first in the panel. */
export function shellNotifications() {
return db.notifications
.all()
.sort((a, b) => b.at.getTime() - a.at.getTime())
.slice(0, 6)
.map(({ id, title, description, at, read, href }) => ({ id, title, description, at, read, href }))
}Its page
On its page the card sits among the rest of the dashboard and shares its range and its data with them.
From the Revenue dashboard page