Complete Guide to SaaS Analytics: Metrics, Charts, and Dashboards
Analytics is what separates hobby projects from serious SaaS businesses. Understanding your metrics — monthly recurring revenue (MRR), churn rate, customer acquisition cost (CAC), lifetime value (LTV), and conversion rates — is essential for making data-driven decisions.
Building a SaaS analytics dashboard requires collecting the right data, computing meaningful metrics, and presenting them in clear visualizations. This guide covers the complete analytics stack for a SaaS application using Next.js 14, Recharts, and your PostgreSQL database.
Key SaaS Metrics
Every SaaS dashboard should track these core metrics:
- Monthly Recurring Revenue (MRR): predictable monthly subscription revenue
- Annual Run Rate (ARR): MRR projected over 12 months
- Churn Rate: percentage of customers who cancel each month
- Customer Lifetime Value (LTV): average revenue per customer over their lifetime
- Customer Acquisition Cost (CAC): cost to acquire a new customer
- Net Revenue Retention (NRR): revenue retained from existing customers including upgrades
- Active Users: daily/weekly/monthly active users (DAU/WAU/MAU)
Database Analytics Queries
// lib/analytics/queries.ts
import { prisma } from '@/lib/prisma'
export async function getMRR() {
const activeSubscriptions = await prisma.user.findMany({
where: {
stripeSubscriptionStatus: { in: ['active', 'trialing'] },
},
select: {
stripePriceId: true,
},
})
// In production, map price IDs to actual amounts
const MRR = activeSubscriptions.length * 79 // Average subscription price
return { mrr: MRR, subscribers: activeSubscriptions.length }
}
export async function getChurnRate(month: Date) {
const startOfMonth = new Date(month.getFullYear(), month.getMonth(), 1)
const startOfNextMonth = new Date(month.getFullYear(), month.getMonth() + 1, 1)
const subscribersAtStart = await prisma.user.count({
where: {
stripeSubscriptionStatus: { in: ['active', 'trialing'] },
createdAt: { lt: startOfMonth },
},
})
const churned = await prisma.user.count({
where: {
stripeSubscriptionStatus: 'canceled',
updatedAt: { gte: startOfMonth, lt: startOfNextMonth },
},
})
return {
churnRate: subscribersAtStart > 0 ? (churned / subscribersAtStart) * 100 : 0,
churned,
total: subscribersAtStart,
}
}
export async function getRevenueGrowth() {
const sixMonthsAgo = new Date()
sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6)
const monthlyRevenue = await prisma.payment.groupBy({
by: ['createdAt'],
_sum: { amount: true },
where: {
createdAt: { gte: sixMonthsAgo },
status: 'completed',
},
})
return monthlyRevenue.map(r => ({
month: r.createdAt.toLocaleDateString('en-US', { month: 'short' }),
revenue: r._sum.amount ?? 0,
}))
}Server Component Analytics Page
// app/dashboard/analytics/page.tsx
import { Suspense } from 'react'
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
import { getMRR, getChurnRate, getRevenueGrowth } from '@/lib/analytics/queries'
import { MetricsGrid } from './MetricsGrid'
import { RevenueChart } from './RevenueChart'
import { CohortChart } from './CohortChart'
export default async function AnalyticsPage() {
const session = await auth()
if (!session?.user) redirect('/login')
const [mrr, churn, revenue] = await Promise.all([
getMRR(),
getChurnRate(new Date()),
getRevenueGrowth(),
])
return (
<div className="space-y-6">
<h1 className="text-2xl font-bold">Analytics</h1>
<MetricsGrid
metrics={[
{ label: 'MRR', value: `$${mrr.mrr.toLocaleString()}`, change: '+12.5%' },
{ label: 'Subscribers', value: mrr.subscribers.toString(), change: '+8.3%' },
{ label: 'Churn Rate', value: `${churn.churnRate.toFixed(1)}%`, change: '-2.1%' },
{ label: 'LTV', value: '$948', change: '+15.2%' },
]}
/>
<Suspense fallback={<div>Loading chart...</div>}>
<RevenueChart data={revenue} />
</Suspense>
<Suspense fallback={<div>Loading cohort...</div>}>
<CohortChart />
</Suspense>
</div>
)
}Revenue Chart with Recharts
'use client'
import {
AreaChart, Area, XAxis, YAxis, CartesianGrid,
Tooltip, ResponsiveContainer,
} from 'recharts'
export function RevenueChart({ data }: { data: { month: string; revenue: number }[] }) {
return (
<div className="rounded-xl border bg-white p-6">
<h3 className="font-semibold">Revenue Growth</h3>
<ResponsiveContainer width="100%" height={350}>
<AreaChart data={data}>
<defs>
<linearGradient id="revenueGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.1} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis dataKey="month" stroke="#888" fontSize={12} />
<YAxis
stroke="#888"
fontSize={12}
tickFormatter={(value) => `$${value.toLocaleString()}`}
/>
<Tooltip
formatter={(value: number) => [`$${value.toLocaleString()}`, 'Revenue']}
/>
<Area
type="monotone"
dataKey="revenue"
stroke="#6366f1"
strokeWidth={2}
fill="url(#revenueGradient)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
)
}Cohort Retention Chart
'use client'
export function CohortChart() {
// In production, this data comes from cohort analysis queries
const cohorts = [
{ month: 'Jan', week1: 100, week2: 65, week3: 52, week4: 48 },
{ month: 'Feb', week1: 100, week2: 68, week3: 55, week4: 50 },
{ month: 'Mar', week1: 100, week2: 72, week3: 58, week4: 53 },
{ month: 'Apr', week1: 100, week2: 70, week3: 56, week4: 51 },
{ month: 'May', week1: 100, week2: 74, week3: 60, week4: 55 },
{ month: 'Jun', week1: 100, week2: 76, week3: 62, week4: 58 },
]
return (
<div className="rounded-xl border bg-white p-6">
<h3 className="font-semibold">Cohort Retention</h3>
<div className="mt-4 overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b">
<th className="py-2 pr-4 text-left font-medium text-gray-500">Cohort</th>
<th className="px-3 py-2 text-center font-medium text-gray-500">Week 1</th>
<th className="px-3 py-2 text-center font-medium text-gray-500">Week 2</th>
<th className="px-3 py-2 text-center font-medium text-gray-500">Week 3</th>
<th className="px-3 py-2 text-center font-medium text-gray-500">Week 4</th>
</tr>
</thead>
<tbody>
{cohorts.map(c => (
<tr key={c.month} className="border-b last:border-0">
<td className="py-2.5 pr-4 font-medium">{c.month}</td>
{[c.week1, c.week2, c.week3, c.week4].map((val, i) => (
<td key={i} className="px-3 py-2.5 text-center">
<span
className="inline-block w-full rounded px-2 py-1 font-medium"
style={{
backgroundColor: `rgba(99, 102, 241, ${val / 100})`,
color: val > 60 ? 'white' : '#374151',
}}
>
{val}%
</span>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}The Bottom Line
SaaS analytics is not optional — it is how you understand your business. The Analytics Dashboard Starter template provides MRR tracking, churn analysis, revenue charts, and cohort retention tables, all connected to your Stripe subscription data and ready to deploy.
Related Template
Try this production-ready starter kit to build your project faster.
Analytics
12,482
+8.2%
84,291
+12.5%
$24,850
+18.3%
3.42%
+2.1%
Traffic Overview
Top Pages
| Page | Views | Avg. Time | Bounce Rate |
|---|---|---|---|
| /dashboard | 12,450 | 4m 32s | 32.1% |
| /pricing | 8,234 | 2m 15s | 45.3% |
| /features | 6,789 | 3m 42s | 28.9% |
| /blog | 5,432 | 5m 10s | 22.4% |
Analytics Dashboard
Visualize your data beautifully
Ready to Build?
Get started with our production-ready starter kits and ship your project faster.
Browse Starter Kits