Most SaaS founders find out their monthly recurring revenue from a Stripe dashboard that was not designed for decision-making. You see a total. You do not know if this month is better or worse than last, how many customers you lost, or what the average customer is worth over a lifetime. Tools like Baremetrics and ChartMogul solve this, but they charge $150 a month before you have proven your product.
If you are already using Stripe subscriptions in your Next.js app, you have the data. It just needs to be surfaced.
Before building anything, decide which metrics you need first:
Everything else -- LTV, ARPU, net revenue retention -- is a function of these three. Start there and add complexity only when it would change a decision.
If you set up Stripe subscriptions, your app already handles customer.subscription.created, customer.subscription.updated, and customer.subscription.deleted webhooks. Each event carries the customer's plan, amount, and status.
The missing piece is a table that logs each status change with a timestamp. Without it, you can only see the current state -- not the history you need for trend lines.
A minimal addition to your Drizzle schema:
export const subscriptionEventTable = pgTable('subscription_events', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => userTable.id),
event: text('event').notNull(), // 'created' | 'updated' | 'canceled'
planId: text('plan_id'),
amountCents: integer('amount_cents'), // normalized to monthly
createdAt: timestamp('created_at').defaultNow(),
});
Your Stripe webhook handler inserts a row on each relevant event. That is the entire data collection step -- no cron jobs, no external queue.
MRR is the sum of amount_cents for all subscriptions where event = 'created' and there is no later event = 'canceled' for the same user. Monthly MRR over time is a GROUP BY month on created_at. That is the series Recharts needs to draw a line chart.
You do not need a separate analytics database. The same Neon DB that stores your users stores your subscription history, and PostgreSQL can aggregate it in milliseconds.
Churn rate for a given month = customers who canceled / customers active at the start of that month.
A 5% monthly churn rate means you lose half your customers in about a year. A 2% rate nearly doubles lifetime value. That single number changes how you think about CAC, marketing spend, and whether it makes sense to run a paid acquisition channel at all.
The dashboard is a Next.js server component. It runs three queries in parallel and renders with data -- no loading spinners, no client-side fetching:
const [mrr, churnRate, newVsCanceled] = await Promise.all([
subscriptionRepo.getMRRByMonth(),
subscriptionRepo.getChurnRateByMonth(),
subscriptionRepo.getNewVsCanceledByMonth(),
]);
Each query returns { month: string; value: number }[]. Pass them directly to Recharts <LineChart> or <BarChart> components.
Protect the route with a role check so only admins see it:
const user = await getUserFromRequest(req);
if (user.role !== 'admin') throw new HttpError(403, 'Forbidden');
Baremetrics connects to Stripe and runs the same aggregations you just built. It costs money for the convenience. Here is the honest decision framework:
For most indie hackers and early-stage SaaS founders, the internal dashboard wins on cost and flexibility.
If you track nothing else, track this: new subscribers minus canceled subscribers, by month.
When that number is positive and growing, you have product-market fit in motion. When it goes flat or negative, you have a problem no dashboard will fix -- but at least you will know before you run out of runway.
The Next.js SaaS boilerplate ships with Stripe webhooks, Drizzle ORM, Recharts, and JWT auth already wired. You are not starting from zero. You are adding one table, three queries, and a protected page.
Get started at boilerplate.iteam-company.com and ship your metrics dashboard this weekend.