NPS Survey Implementation
NPS (Net Promoter Score) is loyalty measurement standard: "How likely to recommend to friends?" on 0–10 scale. Promoters (9–10), Passives (7–8), Detractors (0–6). NPS = % Promoters − % Detractors.
When and Who to Show
Timing impacts data quality:
- After key event: order completion, trial end, first successful feature use
- By time: 14–30 days after signup, max once per 90 days per user
- Segmentation: don't show new users (< 7 days), exclude open support tickets
Backend: Model and API
// Database
Schema::create('nps_responses', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('session_id')->nullable();
$table->tinyInteger('score')->unsigned(); // 0-10
$table->text('comment')->nullable();
$table->string('trigger_event')->nullable(); // 'order_completed'
$table->string('page_url')->nullable();
$table->ipAddress('ip')->nullable();
$table->timestamps();
});
// Controller
public function store(Request $request): JsonResponse
{
$validated = $request->validate([
'score' => 'required|integer|min:0|max:10',
'comment' => 'nullable|string|max:1000',
'trigger_event' => 'nullable|string|max:100',
]);
NpsResponse::create([
'user_id' => Auth::id(),
'score' => $validated['score'],
'comment' => $validated['comment'],
'trigger_event' => $validated['trigger_event'],
'page_url' => $request->referrer(),
'ip' => $request->ip(),
]);
return response()->json(['success' => true]);
}
Frontend Component
function NPSSurvey({ onClose }: { onClose: () => void }) {
const [score, setScore] = useState<number | null>(null);
const [comment, setComment] = useState('');
async function handleSubmit() {
await fetch('/api/nps/submit', {
method: 'POST',
body: JSON.stringify({ score, comment }),
});
onClose();
}
return (
<div className="fixed bottom-6 right-6 bg-white p-6 rounded-lg shadow-lg max-w-sm">
<p className="font-semibold mb-4">How likely to recommend us?</p>
<div className="flex gap-2 mb-4">
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(n => (
<button
key={n}
onClick={() => setScore(n)}
className={`w-8 h-8 rounded ${
score === n ? 'bg-blue-600 text-white' : 'bg-gray-100'
}`}
>
{n}
</button>
))}
</div>
{score !== null && score < 7 && (
<textarea
placeholder="How can we improve?"
value={comment}
onChange={e => setComment(e.target.value)}
className="w-full border rounded p-2 mb-4"
/>
)}
<button
onClick={handleSubmit}
className="bg-blue-600 text-white px-4 py-2 rounded w-full"
>
Submit
</button>
</div>
);
}
Analysis
SELECT
CASE
WHEN score >= 9 THEN 'Promoter'
WHEN score >= 7 THEN 'Passive'
ELSE 'Detractor'
END AS category,
COUNT(*) AS count,
ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER (), 1) AS pct
FROM nps_responses
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY 1;
Timeline
Basic survey widget—2–3 days. With intelligent scheduling and dashboard—5–7 days.







