Product-Market Fit: Technical Guide to Validate Your Startup
Complete methodology to achieve Product-Market Fit: metrics, signals, experiments, analytics stack. Sean Ellis framework + concrete examples.
Product-Market Fit: Technical Guide to Validate Your Startup
90% of startups fail before Product-Market Fit. Here's the complete guide (technical + methodology) to know if you've found PMF, and how to measure it rigorously.
Product-Market Fit: Technical Definition
Marc Andreessen's Formula
"Product-Market Fit = when your product solves an urgent problem for a specific market, and that market pulls you rather than you pushing it."
Technical Signals:
- ✅ Cohort retention >40% M1
- ✅ NPS >40
- ✅ CAC payback <12 months
- ✅ Organic growth >20%/month
- ✅ Churn <5%/month
The 3 Phases Before PMF
Phase 1: Problem-Solution Fit (3-6 months)
│ Goal: Validate that the problem exists
│ Metric: 100 interviews, 10+ early adopters
│
├─► Phase 2: Product-Solution Fit (6-12 months)
│ Goal: Build usable MVP
│ Metric: 10 paying users, retention >30%
│
├─► Phase 3: Product-Market Fit (12-24 months)
Goal: Scaling channel acquisition
Metric: 100+ customers, NPS >40, churn <5%
This guide focuses Phase 3: measure and optimize PMF
Sean Ellis Test: Are You PMF?
The Single Question
Survey to Send:
"How would you feel if you could no longer use [Product]?"
- Very disappointed
- Somewhat disappointed
- Not really disappointed
- N/A (no longer using)
PMF Threshold: >40% answer "Very disappointed"
Technical Setup (TypeScript)
// lib/pmf-survey.ts
import { sendEmail } from './email';
export async function sendPMFSurvey(userId: string) {
const user = await prisma.user.findUnique({ where: { id: userId } });
// Eligibility: active for 30+ days
const daysSinceSignup =
(Date.now() - user.createdAt.getTime()) / (1000 * 60 * 60 * 24);
if (daysSinceSignup < 30) return;
// Generate unique token
const token = generateToken();
await prisma.pmfSurvey.create({
data: { userId, token, sentAt: new Date() }
});
// Send email
await sendEmail({
to: user.email,
subject: "Quick question about [Product]",
html: `
<p>Hi ${user.name},</p>
<p>We'd love your feedback. How would you feel if you could no longer use [Product]?</p>
<a href="https://app.com/survey/${token}?answer=very_disappointed">Very disappointed</a><br>
<a href="https://app.com/survey/${token}?answer=somewhat_disappointed">Somewhat disappointed</a><br>
<a href="https://app.com/survey/${token}?answer=not_disappointed">Not disappointed</a>
`
});
}
Trigger: Daily cron job
// app/api/cron/pmf-survey/route.ts
export async function GET(req: Request) {
// Auth Vercel Cron
if (req.headers.get('Authorization') !== `Bearer ${process.env.CRON_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
// Eligible users: active 30+ days, not yet surveyed
const users = await prisma.user.findMany({
where: {
createdAt: { lte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
pmfSurveys: { none: {} }
},
take: 50 // Batch of 50/day
});
for (const user of users) {
await sendPMFSurvey(user.id);
}
return Response.json({ sent: users.length });
}
Vercel Cron Config:
// vercel.json
{
"crons": [{
"path": "/api/cron/pmf-survey",
"schedule": "0 10 * * *"
}]
}
Results Dashboard
// app/admin/pmf/page.tsx
export default async function PMFDashboard() {
const surveys = await prisma.pmfSurvey.findMany({
where: { answeredAt: { not: null } }
});
const total = surveys.length;
const veryDisappointed = surveys.filter(s => s.answer === 'very_disappointed').length;
const score = (veryDisappointed / total) * 100;
return (
<div>
<h1>PMF Score</h1>
<div className="text-6xl font-bold">
{score.toFixed(1)}%
</div>
<p>{veryDisappointed}/{total} "Very disappointed"</p>
<p className={score > 40 ? 'text-green-600' : 'text-red-600'}>
{score > 40 ? '✅ PMF achieved' : '❌ Not yet PMF'}
</p>
</div>
);
}
Complete Analytics Stack for PMF
Layer 1: Product Analytics (Usage)
Tools:
| Tool | Usage | Price/month | Setup |
|---|---|---|---|
| PostHog | Analytics + feature flags | 0-50€ | 1h |
| Mixpanel | Funnels + cohorts | 0-100€ | 2h |
| Amplitude | Retention + behavior | 0-200€ | 2h |
Recommendation: PostHog (open-source, self-hosted possible)
PostHog Setup:
// lib/posthog.ts
import posthog from 'posthog-js';
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: 'https://app.posthog.com',
loaded: (posthog) => {
if (process.env.NODE_ENV === 'development') posthog.debug();
}
});
}
export default posthog;
Tracking Events:
// components/SignupForm.tsx
import posthog from '@/lib/posthog';
function handleSubmit(data) {
// Create user
const user = await createUser(data);
// Track event
posthog.capture('user_signed_up', {
userId: user.id,
plan: user.plan,
source: data.referralSource
});
// Identify user
posthog.identify(user.id, {
email: user.email,
name: user.name,
createdAt: user.createdAt
});
}
Critical Events to Track:
// Core activation events
posthog.capture('onboarding_completed');
posthog.capture('first_project_created');
posthog.capture('invited_team_member');
posthog.capture('first_payment');
// Engagement events
posthog.capture('feature_used', { featureName: 'export_pdf' });
posthog.capture('session_started');
posthog.capture('session_ended', { duration: 1200 }); // 20min
Layer 2: Business Metrics (Revenue)
Stripe Dashboard:
// app/api/metrics/mrr/route.ts
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function GET() {
// MRR (Monthly Recurring Revenue)
const subscriptions = await stripe.subscriptions.list({
status: 'active',
limit: 100
});
const mrr = subscriptions.data.reduce((sum, sub) => {
return sum + (sub.items.data[0].price.unit_amount! / 100);
}, 0);
// Churn rate (last 30 days)
const canceledSubs = await stripe.subscriptions.list({
status: 'canceled',
created: { gte: Math.floor(Date.now() / 1000) - 30 * 24 * 60 * 60 }
});
const churnRate = (canceledSubs.data.length / subscriptions.data.length) * 100;
return Response.json({ mrr, churnRate });
}
Layer 3: User Feedback (Qualitative)
Tools:
- Typeform: NPS surveys (0-50€/month)
- Hotjar: Heatmaps + recordings (0-80€/month)
- Plain: Customer support + feedback (0-100€/month)
NPS Survey Automation:
// Send NPS survey after 60 days
export async function sendNPSSurvey(userId: string) {
const user = await prisma.user.findUnique({ where: { id: userId } });
const daysSinceSignup =
(Date.now() - user.createdAt.getTime()) / (1000 * 60 * 60 * 24);
if (daysSinceSignup < 60) return;
await sendEmail({
to: user.email,
subject: "How likely are you to recommend [Product]?",
html: `
<p>On a scale of 0-10, how likely are you to recommend [Product] to a friend?</p>
${[...Array(11)].map((_, i) =>
`<a href="https://app.com/nps/${token}?score=${i}">${i}</a>`
).join(' ')}
`
});
}
NPS Calculation:
// NPS = % Promoters (9-10) - % Detractors (0-6)
export function calculateNPS(scores: number[]) {
const promoters = scores.filter(s => s >= 9).length;
const detractors = scores.filter(s => s <= 6).length;
return ((promoters - detractors) / scores.length) * 100;
}
PMF Metrics: Complete Dashboard
Metric #1: Cohort Retention
Definition: % users active N days after signup
// lib/metrics/retention.ts
export async function calculateRetention(cohortDate: Date, dayN: number) {
// Users signed up that day
const cohortUsers = await prisma.user.count({
where: {
createdAt: {
gte: cohortDate,
lt: new Date(cohortDate.getTime() + 24 * 60 * 60 * 1000)
}
}
});
// Users active N days later
const activeUsers = await prisma.user.count({
where: {
createdAt: {
gte: cohortDate,
lt: new Date(cohortDate.getTime() + 24 * 60 * 60 * 1000)
},
lastActiveAt: {
gte: new Date(cohortDate.getTime() + dayN * 24 * 60 * 60 * 1000),
lt: new Date(cohortDate.getTime() + (dayN + 1) * 24 * 60 * 60 * 1000)
}
}
});
return (activeUsers / cohortUsers) * 100;
}
Benchmark:
| Day | SaaS B2B | SaaS B2C | Marketplace |
|---|---|---|---|
| D1 | 60-80% | 40-60% | 30-50% |
| D7 | 40-60% | 20-40% | 15-30% |
| D30 | 30-50% | 10-25% | 10-20% |
| D90 | 20-40% | 5-15% | 5-15% |
PMF Threshold: D30 Retention >40% (B2B) or >25% (B2C)
Metric #2: Churn Rate
Definition: % customers lost/month
export async function calculateChurn(month: Date) {
const startOfMonth = new Date(month.getFullYear(), month.getMonth(), 1);
const endOfMonth = new Date(month.getFullYear(), month.getMonth() + 1, 0);
// Customers at start of month
const customersStart = await prisma.subscription.count({
where: {
status: 'active',
createdAt: { lt: startOfMonth }
}
});
// Customers who churned this month
const churned = await prisma.subscription.count({
where: {
status: 'canceled',
canceledAt: {
gte: startOfMonth,
lte: endOfMonth
}
}
});
return (churned / customersStart) * 100;
}
Benchmark:
- ✅ Excellent: <5%/month
- ⚠️ Acceptable: 5-10%/month
- ❌ Bad: >10%/month
PMF Threshold: Churn <5%/month
Metric #3: NPS (Net Promoter Score)
Calculation: % Promoters (9-10) - % Detractors (0-6)
Benchmark:
- ✅ World-class: >70 (Apple, Tesla)
- ✅ Excellent: 50-70 (Netflix, Airbnb)
- ✅ Good: 30-50 (PMF startups)
- ⚠️ Average: 0-30
- ❌ Bad: <0
PMF Threshold: NPS >40
Metric #4: CAC Payback
Definition: Time to recover acquisition cost
export function calculateCACPayback(
cac: number, // 500€
arpu: number, // 50€/month
grossMargin: number // 80%
) {
const monthlyProfit = arpu * (grossMargin / 100);
return cac / monthlyProfit; // = 12.5 months
}
Benchmark:
- ✅ Excellent: <6 months
- ✅ Good: 6-12 months
- ⚠️ Acceptable: 12-18 months
- ❌ Bad: >18 months
PMF Threshold: CAC Payback <12 months
Metric #5: Organic Growth
Definition: % signups without paid ads
export async function calculateOrganicGrowth(month: Date) {
const signups = await prisma.user.count({
where: {
createdAt: {
gte: new Date(month.getFullYear(), month.getMonth(), 1),
lt: new Date(month.getFullYear(), month.getMonth() + 1, 0)
}
}
});
const organicSignups = await prisma.user.count({
where: {
createdAt: {
gte: new Date(month.getFullYear(), month.getMonth(), 1),
lt: new Date(month.getFullYear(), month.getMonth() + 1, 0)
},
source: { in: ['organic', 'direct', 'referral'] }
}
});
return (organicSignups / signups) * 100;
}
Benchmark:
- ✅ Strong PMF: >50% organic
- ⚠️ Weak PMF: 20-50% organic
- ❌ No PMF: <20% organic
PMF Threshold: >20% monthly organic growth
PMF Dashboard: React Template
// app/admin/pmf-dashboard/page.tsx
export default async function PMFDashboard() {
const [retention, churn, nps, cac, organic] = await Promise.all([
calculateRetention(new Date(), 30),
calculateChurn(new Date()),
calculateNPS(),
calculateCACPayback(),
calculateOrganicGrowth(new Date())
]);
const pmfScore =
(retention > 40 ? 20 : 0) +
(churn < 5 ? 20 : 0) +
(nps > 40 ? 20 : 0) +
(cac < 12 ? 20 : 0) +
(organic > 20 ? 20 : 0);
return (
<div className="grid grid-cols-3 gap-4">
<MetricCard
title="D30 Retention"
value={`${retention.toFixed(1)}%`}
target=">40%"
status={retention > 40 ? 'good' : 'bad'}
/>
<MetricCard
title="Churn"
value={`${churn.toFixed(1)}%`}
target="<5%"
status={churn < 5 ? 'good' : 'bad'}
/>
<MetricCard
title="NPS"
value={nps.toFixed(0)}
target=">40"
status={nps > 40 ? 'good' : 'bad'}
/>
<MetricCard
title="CAC Payback"
value={`${cac.toFixed(1)} months`}
target="<12 months"
status={cac < 12 ? 'good' : 'bad'}
/>
<MetricCard
title="Organic Growth"
value={`${organic.toFixed(1)}%`}
target=">20%"
status={organic > 20 ? 'good' : 'bad'}
/>
<div className="col-span-3 text-center">
<h2 className="text-2xl font-bold">PMF Score</h2>
<div className={`text-8xl font-bold ${
pmfScore === 100 ? 'text-green-600' :
pmfScore >= 60 ? 'text-yellow-600' :
'text-red-600'
}`}>
{pmfScore}/100
</div>
<p>
{pmfScore === 100 && '🎉 PMF achieved!'}
{pmfScore >= 60 && pmfScore < 100 && '⚠️ Close to PMF'}
{pmfScore < 60 && '❌ Not yet PMF'}
</p>
</div>
</div>
);
}
Experiments to Achieve PMF
AARRR Framework (Pirate Metrics)
Acquisition → Activation → Retention → Referral → Revenue
Optimize Each Step:
1. Acquisition
Hypothesis: "Landing page with demo video increases conversion" Experiment:
- Variant A: Without video
- Variant B: With 60s video
Measure: Signup conversion Tool: Vercel A/B testing
// app/page.tsx
import { unstable_flag as flag } from '@vercel/flags/next';
export default function Home() {
const showVideo = flag('show-video-demo');
return (
<>
<h1>Welcome</h1>
{showVideo && <VideoDemo src="/demo.mp4" />}
<SignupCTA />
</>
);
}
2. Activation
Hypothesis: "Interactive onboarding increases activation" Experiment:
- Variant A: Static onboarding
- Variant B: Step-by-step interactive onboarding
Measure: % users who complete onboarding Target: >60%
3. Retention
Hypothesis: "D+3 email reminder increases D7 retention" Experiment:
- Cohort A: No email
- Cohort B: D+3 email
Measure: D7 retention Target: +10% vs control
4. Referral
Hypothesis: "Referral program increases organic signups" Experiment:
- Offer 1 free month per referral
Measure: % signups via referral Target: >15%
5. Revenue
Hypothesis: "Pricing 49€/month vs 99€/month" Experiment:
- Cohort A: 49€/month
- Cohort B: 99€/month
Measure: Trial → paid conversion Target: Maximize LTV (Lifetime Value)
Real Cases: Before/After PMF
✅ Success Case: Notion
Before PMF (2016-2018):
- D30 Retention: 15%
- Churn: 12%/month
- Growth: 100% paid ads
Pivot:
- Focus template gallery (activation)
- Web clipper (daily hook)
- Generous free tier
After PMF (2019+):
- D30 Retention: 65%
- Churn: 3%/month
- Growth: 70% organic
- Valuation: $10B
❌ Failure Case: Quibi
Launch Metrics (2020):
- $1.75B raised
- D7 Retention: 8% (catastrophic)
- Churn: 90%/month
- NPS: <0
Problem: No PMF (mobile-only content = fake need) Result: Shutdown 6 months after launch
PMF Analytics Budget
Minimum Stack (0-50 users)
| Tool | Price/month |
|---|---|
| PostHog (self-hosted) | 0€ |
| Stripe Dashboard | 0€ |
| Google Forms (surveys) | 0€ |
| TOTAL | 0€ |
Recommended Stack (50-500 users)
| Tool | Price/month |
|---|---|
| PostHog Cloud | 50€ |
| Typeform | 30€ |
| Hotjar | 40€ |
| Plain (support) | 50€ |
| TOTAL | 170€ |
Advanced Stack (500+ users)
| Tool | Price/month |
|---|---|
| Amplitude | 200€ |
| Segment (CDP) | 120€ |
| Zendesk | 100€ |
| Looker (BI) | 300€ |
| TOTAL | 720€ |
Conclusion
PMF is not binary, it's a spectrum (0 → 100).
Minimum Threshold for Series A raise:
- ✅ D30 Retention >40%
- ✅ Churn <5%/month
- ✅ NPS >40
- ✅ CAC Payback <12 months
- ✅ Organic Growth >20%
Analytics Setup: 2 weeks dev + 170€/month. ROI: Gain 6-12 months vs intuition.
PMF Audit: I analyze your metrics and tell you if you're ready to scale.
About: Jérémy Marquer has helped 15 startups achieve PMF. Data-driven method, no bullshit.
Related articles
Choosing Your Startup Tech Stack 2025: Decision Guide (Next.js, React, Python)
Complete framework to choose startup tech stack: Next.js vs React, Node vs Python, PostgreSQL vs MongoDB. Criteria, benchmarks, costs, mistakes to avoid.
Scale a Tech Startup: From 10 to 100 Users Without Exploding
Practical guide to scale infrastructure, team and processes from 10 to 100 users. Architecture, monitoring, technical debt, budget. Avoid the pitfalls.
Automate Your Startup Processes: AI, No-Code and Productivity Gains in 2025
How to automate startup processes with AI and No-Code? Complete guide for CTOs and founders: tools, ROI, use cases, pitfalls to avoid.
