Waarom SaaS-metrics er écht toe doen
Je hebt je SaaS gelanceerd, de eerste klanten stromen binnen, en je MRR groeit. Maar weet je ook waarom het groeit? En nog belangrijker: zie je de waarschuwingssignalen voordat het te laat is?
De meeste SaaS-founders bouwen hun product op gevoel. Ze checken Stripe af en toe, kijken naar het aantal gebruikers in de database, en hopen dat het goed gaat. Maar data-gedreven beslissingen maken het verschil tussen een SaaS die schaalt en een die stilletjes leegloopt.
In dit artikel bouwen we stap voor stap een analytics-systeem voor je SaaS: van de juiste metrics tot een werkend dashboard.
De 7 metrics die elke SaaS moet tracken
1. Monthly Recurring Revenue (MRR)
MRR is de hartslag van je SaaS. Het is de voorspelbare, maandelijks terugkerende omzet van al je actieve abonnementen.
-- MRR berekenen vanuit je subscriptions tabel
SELECT
DATE_TRUNC('month', current_period_start) AS month,
SUM(
CASE
WHEN interval = 'year' THEN price / 12
ELSE price
END
) AS mrr
FROM subscriptions
WHERE status = 'active'
GROUP BY month
ORDER BY month DESC;
Belangrijk: splits je MRR op in componenten:
- New MRR — nieuwe klanten
- Expansion MRR — upgrades en uitbreidingen
- Contraction MRR — downgrades
- Churned MRR — opgezegde abonnementen
2. Churn Rate
Churn is de stille killer van SaaS-bedrijven. Er zijn twee soorten:
// Customer churn rate
const customerChurnRate = (lostCustomers / startCustomers) * 100;
// Revenue churn rate (belangrijker!)
const revenueChurnRate = (lostMRR / startMRR) * 100;
// Net revenue churn (inclusief expansie)
const netRevenueChurn = ((lostMRR - expansionMRR) / startMRR) * 100;
Een negatieve net revenue churn is de heilige graal: je bestaande klanten groeien sneller dan je verliest.
3. Customer Lifetime Value (LTV)
// Simpele LTV-berekening
const avgRevenuePerAccount = totalMRR / totalCustomers;
const avgLifetimeMonths = 1 / (customerChurnRate / 100);
const ltv = avgRevenuePerAccount * avgLifetimeMonths;
// LTV:CAC ratio moet minimaal 3:1 zijn
const ltvCacRatio = ltv / customerAcquisitionCost;
4. Customer Acquisition Cost (CAC)
Tel al je sales- en marketingkosten op en deel door het aantal nieuwe klanten. Vergeet niet: salarissen, tooling, advertenties, en content-productie tellen allemaal mee.
5. Activation Rate
Hoeveel procent van nieuwe aanmeldingen bereikt het "aha-moment"? Dit is vaak de meest onderschatte metric.
-- Activation rate: gebruikers die binnen 7 dagen een kernactie voltooien
SELECT
DATE_TRUNC('week', u.created_at) AS cohort_week,
COUNT(DISTINCT u.id) AS signups,
COUNT(DISTINCT e.user_id) AS activated,
ROUND(
COUNT(DISTINCT e.user_id)::numeric / COUNT(DISTINCT u.id) * 100, 1
) AS activation_rate
FROM users u
LEFT JOIN events e ON e.user_id = u.id
AND e.name = 'core_action_completed'
AND e.created_at <= u.created_at + INTERVAL '7 days'
WHERE u.created_at >= NOW() - INTERVAL '3 months'
GROUP BY cohort_week
ORDER BY cohort_week DESC;
6. Daily/Weekly/Monthly Active Users (DAU/WAU/MAU)
-- DAU/MAU ratio (stickiness)
WITH daily AS (
SELECT COUNT(DISTINCT user_id) AS dau
FROM events
WHERE created_at >= CURRENT_DATE
),
monthly AS (
SELECT COUNT(DISTINCT user_id) AS mau
FROM events
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
)
SELECT
dau,
mau,
ROUND(dau::numeric / NULLIF(mau, 0) * 100, 1) AS stickiness
FROM daily, monthly;
Een DAU/MAU ratio boven 20% is goed; boven 50% is uitzonderlijk.
7. Time to Value (TTV)
Hoe lang duurt het voordat een nieuwe gebruiker waarde ervaart? Meet dit per cohort en optimaliseer je onboarding om TTV te verkorten.
Architectuur: een event-driven analytics pipeline
De beste manier om SaaS-analytics te bouwen is met een event-based systeem. Elke gebruikersactie wordt een event.
Event tracking implementeren
// lib/analytics.ts
interface AnalyticsEvent {
name: string;
userId: string;
tenantId: string;
properties?: Record<string, unknown>;
timestamp?: Date;
}
class Analytics {
private queue: AnalyticsEvent[] = [];
private flushInterval: NodeJS.Timeout;
constructor(private batchSize = 50, private intervalMs = 5000) {
this.flushInterval = setInterval(() => this.flush(), intervalMs);
}
track(event: AnalyticsEvent) {
this.queue.push({
...event,
timestamp: event.timestamp ?? new Date(),
});
if (this.queue.length >= this.batchSize) {
this.flush();
}
}
private async flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, this.batchSize);
try {
await db.analyticsEvent.createMany({
data: batch.map(e => ({
name: e.name,
userId: e.userId,
tenantId: e.tenantId,
properties: e.properties ?? {},
timestamp: e.timestamp,
})),
});
} catch (error) {
// Bij falen: events terug in de queue
this.queue.unshift(...batch);
console.error('Analytics flush failed:', error);
}
}
}
export const analytics = new Analytics();
Events tracken in je applicatie
// In je API routes of server actions
import { analytics } from '@/lib/analytics';
// Gebruiker maakt een project aan
analytics.track({
name: 'project_created',
userId: user.id,
tenantId: user.tenantId,
properties: {
projectType: 'kanban',
teamSize: team.members.length,
},
});
// Gebruiker upgradet
analytics.track({
name: 'subscription_upgraded',
userId: user.id,
tenantId: user.tenantId,
properties: {
fromPlan: 'starter',
toPlan: 'professional',
mrrDelta: 50,
},
});
Het dashboard bouwen
Optie 1: Zelf bouwen met SQL + charting library
Voor een interne dashboard is een combinatie van SQL-views en een chart library als Recharts of Tremor ideaal.
// app/api/analytics/mrr/route.ts
import { db } from '@/lib/db';
export async function GET() {
const mrrData = await db.$queryRaw`
WITH monthly_mrr AS (
SELECT
DATE_TRUNC('month', s."currentPeriodStart") AS month,
SUM(CASE
WHEN s.interval = 'year' THEN s.price / 12
ELSE s.price
END) AS mrr
FROM subscriptions s
WHERE s.status = 'active'
GROUP BY month
)
SELECT
month,
mrr,
mrr - LAG(mrr) OVER (ORDER BY month) AS mrr_change,
ROUND(
(mrr - LAG(mrr) OVER (ORDER BY month))::numeric
/ NULLIF(LAG(mrr) OVER (ORDER BY month), 0) * 100,
1
) AS growth_pct
FROM monthly_mrr
ORDER BY month DESC
LIMIT 12
`;
return Response.json(mrrData);
}
// components/MRRChart.tsx
'use client';
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
export function MRRChart({ data }: { data: MRRDataPoint[] }) {
return (
<div className="rounded-lg border bg-card p-6">
<h3 className="text-lg font-semibold mb-4">Monthly Recurring Revenue</h3>
<ResponsiveContainer width="100%" height={300}>
<AreaChart data={data}>
<XAxis
dataKey="month"
tickFormatter={(v) => new Date(v).toLocaleDateString('nl-NL', {
month: 'short', year: '2-digit'
})}
/>
<YAxis tickFormatter={(v) => `€${(v / 1000).toFixed(0)}k`} />
<Tooltip
formatter={(v: number) => [`€${v.toLocaleString()}`, 'MRR']}
/>
<Area
type="monotone"
dataKey="mrr"
stroke="#6366f1"
fill="#6366f1"
fillOpacity={0.1}
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}
Optie 2: Externe tools integreren
Voor snellere implementatie kun je kiezen uit:
| Tool | Beste voor | Kosten |
|---|---|---|
| PostHog | Product analytics + feature flags | Gratis tot 1M events/maand |
| Mixpanel | User behavior tracking | Gratis tot 20M events/maand |
| Metabase | SQL-based dashboards | Open source |
| Grafana | Technische metrics | Open source |
PostHog is een populaire keuze omdat het self-hosted kan draaien en combineert met feature flags en session replay:
// lib/posthog.ts
import PostHog from 'posthog-node';
const posthog = new PostHog(process.env.POSTHOG_API_KEY!, {
host: process.env.POSTHOG_HOST ?? 'https://eu.posthog.com',
});
export function trackServerEvent(
userId: string,
event: string,
properties?: Record<string, unknown>
) {
posthog.capture({
distinctId: userId,
event,
properties: {
...properties,
$set: { lastSeen: new Date().toISOString() },
},
});
}
Cohort-analyse: de sleutel tot groei
Cohort-analyse laat je zien hoe groepen gebruikers zich over tijd gedragen. Dit is cruciaal om te begrijpen of je product daadwerkelijk verbetert.
-- Retentie cohort-analyse
WITH cohorts AS (
SELECT
id AS user_id,
DATE_TRUNC('month', created_at) AS cohort_month
FROM users
),
activity AS (
SELECT DISTINCT
user_id,
DATE_TRUNC('month', created_at) AS activity_month
FROM events
)
SELECT
c.cohort_month,
COUNT(DISTINCT c.user_id) AS cohort_size,
COUNT(DISTINCT CASE
WHEN a.activity_month = c.cohort_month + INTERVAL '1 month'
THEN c.user_id
END) AS month_1,
COUNT(DISTINCT CASE
WHEN a.activity_month = c.cohort_month + INTERVAL '2 months'
THEN c.user_id
END) AS month_2,
COUNT(DISTINCT CASE
WHEN a.activity_month = c.cohort_month + INTERVAL '3 months'
THEN c.user_id
END) AS month_3
FROM cohorts c
LEFT JOIN activity a ON a.user_id = c.user_id
GROUP BY c.cohort_month
ORDER BY c.cohort_month DESC;
Als je retentiecurve na een paar maanden afvlakt (in plaats van naar nul te dalen), heb je product-market fit gevonden.
Alerting: reageer voordat het te laat is
Een dashboard is nutteloos als niemand ernaar kijkt. Stel automatische alerts in voor kritieke drempels:
// lib/alerts.ts
interface AlertConfig {
metric: string;
threshold: number;
direction: 'above' | 'below';
channel: 'slack' | 'email';
}
const alerts: AlertConfig[] = [
{ metric: 'daily_churn_rate', threshold: 2, direction: 'above', channel: 'slack' },
{ metric: 'activation_rate', threshold: 30, direction: 'below', channel: 'slack' },
{ metric: 'error_rate_5xx', threshold: 1, direction: 'above', channel: 'slack' },
{ metric: 'mrr_growth', threshold: 0, direction: 'below', channel: 'email' },
];
async function checkAlerts() {
for (const alert of alerts) {
const currentValue = await getMetricValue(alert.metric);
const triggered =
alert.direction === 'above'
? currentValue > alert.threshold
: currentValue < alert.threshold;
if (triggered) {
await notify(alert.channel, {
text: `⚠️ Alert: ${alert.metric} is ${currentValue} (${alert.direction} threshold of ${alert.threshold})`,
});
}
}
}
// Draai elke 6 uur via een cron job
Privacy en GDPR
Analytics en privacy gaan niet altijd makkelijk samen. Belangrijke vuistregels:
- Anonimiseer waar mogelijk — gebruik tenant-level metrics in plaats van user-level tracking voor externe rapportages
- Respecteer Do Not Track — bied een opt-out voor niet-essentiële tracking
- Bewaar data niet te lang — stel een retentiebeleid in (bijv. 24 maanden voor ruwe events)
- Documenteer je verwerkingsgrondslag — analytics valt vaak onder "gerechtvaardigd belang", maar onderbouw dit
// Middleware: respecteer tracking-voorkeuren
function shouldTrack(userId: string, eventType: string): boolean {
// Essentiële metrics (billing, security) altijd tracken
if (['subscription_changed', 'login_failed'].includes(eventType)) {
return true;
}
// Check opt-out preference
return !userPreferences.get(userId)?.analyticsOptOut;
}
Praktische tips voor implementatie
- Begin klein — Track eerst alleen MRR, churn en activation rate. Voeg meer metrics toe als je groeit.
- Automatiseer rapportages — Stuur elke maandag een samenvatting naar Slack met de belangrijkste metrics.
- Maak metrics zichtbaar — Hang een TV-dashboard op kantoor of pin het in je team-channel.
- Vergelijk met benchmarks — Een churn rate van 5% per maand is normaal voor SMB SaaS, maar slecht voor enterprise.
- Scheid operationele en analytische queries — Gebruik een read replica of een apart analytics-schema om je productiedatabase niet te belasten.
Conclusie
Een goed analytics-systeem is geen luxe — het is een noodzaak voor elke serieuze SaaS. Begin met de basis (MRR, churn, activatie), bouw een event-driven pipeline, en breid uit naar cohort-analyse en automatische alerts.
De investering betaalt zich dubbel en dwars terug: je maakt betere beslissingen, ziet problemen eerder, en kunt aan investeerders en stakeholders precies laten zien hoe je SaaS ervoor staat.
Wil je hulp bij het bouwen van een analytics-dashboard voor je SaaS? Neem contact met ons op — we helpen je graag.