Build a Real-Time Dashboard with Mixpanel 2026
How to Build a Real-Time Dashboard with the Mixpanel API
Mixpanel tracks user behavior and lets you query that data via API. This guide shows how to instrument your app with events, then build a custom dashboard that displays funnels, retention, and engagement metrics in real time.
Most SaaS teams start with Mixpanel's built-in dashboard and eventually want to embed analytics data into their own product — showing users their own usage statistics, building internal ops dashboards, or powering data-driven emails ("You've used Feature X 47 times this month!"). Mixpanel's Data Export API makes this possible. The combination of server-side event tracking (for critical business events like purchases) and client-side tracking (for engagement signals like clicks and feature use) gives you a complete picture of user behavior across both your backend logic and frontend interactions.
The guide builds toward a custom dashboard component that queries Mixpanel's API server-side (keeping your service account credentials private) and renders metrics with trend comparisons. The same pattern works for internal ops dashboards, customer-facing usage pages, and automated weekly digest emails generated from Mixpanel data.
What You'll Build
- Event tracking (page views, button clicks, signups, purchases)
- Custom analytics dashboard with charts
- Funnel analysis (signup → activation → conversion)
- Retention cohort visualization
- Real-time event feed
Prerequisites: Node.js 18+, Mixpanel account (free: 20M events/month).
1. Setup
Install
# Server-side tracking
npm install mixpanel
# Client-side tracking
npm install mixpanel-browser
Initialize Server-Side
// lib/mixpanel.ts
import Mixpanel from 'mixpanel';
export const mixpanel = Mixpanel.init(process.env.MIXPANEL_TOKEN!, {
host: 'api.mixpanel.com',
});
Initialize Client-Side
// lib/mixpanel-client.ts
import mixpanel from 'mixpanel-browser';
mixpanel.init(process.env.NEXT_PUBLIC_MIXPANEL_TOKEN!, {
track_pageview: true,
persistence: 'localStorage',
});
export default mixpanel;
2. Track Events
Server-Side Events
// Track signup
mixpanel.track('Sign Up', {
distinct_id: user.id,
email: user.email,
plan: 'free',
source: 'organic',
});
// Track purchase
mixpanel.track('Purchase', {
distinct_id: user.id,
amount: 29.00,
plan: 'pro',
currency: 'USD',
});
// Set user profile
mixpanel.people.set(user.id, {
$email: user.email,
$name: user.name,
plan: 'pro',
signup_date: new Date().toISOString(),
});
Client-Side Events
// components/TrackingProvider.tsx
'use client';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';
import mixpanel from '@/lib/mixpanel-client';
export function TrackingProvider({ children, userId }: {
children: React.ReactNode;
userId?: string;
}) {
const pathname = usePathname();
useEffect(() => {
if (userId) {
mixpanel.identify(userId);
}
}, [userId]);
useEffect(() => {
mixpanel.track('Page View', { path: pathname });
}, [pathname]);
return <>{children}</>;
}
// Track button clicks
export function trackClick(name: string, props?: Record<string, any>) {
mixpanel.track('Button Click', { button_name: name, ...props });
}
Event Naming Best Practices
| Event | Properties | When |
|---|---|---|
Sign Up | plan, source, method | User creates account |
Login | method (email, google, github) | User logs in |
Page View | path, referrer | Every page load |
Feature Used | feature_name, context | User engages with feature |
Purchase | amount, plan, currency | Payment completed |
Subscription Changed | from_plan, to_plan | Upgrade/downgrade |
Churn | reason, days_active | User cancels |
3. Query Data via API
Authentication
Mixpanel uses service account authentication for the Data Export API:
// lib/mixpanel-query.ts
const MIXPANEL_PROJECT_ID = process.env.MIXPANEL_PROJECT_ID!;
const MIXPANEL_SERVICE_ACCOUNT = process.env.MIXPANEL_SERVICE_ACCOUNT!;
const MIXPANEL_SERVICE_SECRET = process.env.MIXPANEL_SERVICE_SECRET!;
const auth = Buffer.from(
`${MIXPANEL_SERVICE_ACCOUNT}:${MIXPANEL_SERVICE_SECRET}`
).toString('base64');
export async function queryMixpanel(endpoint: string, params: Record<string, string>) {
const url = new URL(`https://mixpanel.com/api/query${endpoint}`);
url.searchParams.set('project_id', MIXPANEL_PROJECT_ID);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
const res = await fetch(url.toString(), {
headers: { Authorization: `Basic ${auth}` },
});
return res.json();
}
Query Event Counts
// Get daily signups for the last 30 days
const signupData = await queryMixpanel('/insights', {
event: JSON.stringify([{ event: 'Sign Up' }]),
type: 'general',
unit: 'day',
interval: '30',
});
Funnel Query
const funnelData = await queryMixpanel('/funnels', {
funnel_id: 'your_funnel_id',
from_date: '2026-02-06',
to_date: '2026-03-08',
unit: 'week',
});
4. Build the Dashboard
Dashboard API Route
// app/api/dashboard/route.ts
import { NextResponse } from 'next/server';
import { queryMixpanel } from '@/lib/mixpanel-query';
export async function GET() {
const [signups, purchases, activeUsers] = await Promise.all([
queryMixpanel('/insights', {
event: JSON.stringify([{ event: 'Sign Up' }]),
type: 'general',
unit: 'day',
interval: '30',
}),
queryMixpanel('/insights', {
event: JSON.stringify([{ event: 'Purchase' }]),
type: 'general',
unit: 'day',
interval: '30',
}),
queryMixpanel('/insights', {
event: JSON.stringify([{ event: 'Login' }]),
type: 'unique',
unit: 'day',
interval: '7',
}),
]);
return NextResponse.json({ signups, purchases, activeUsers });
}
Dashboard Component
// app/dashboard/analytics/page.tsx
'use client';
import { useEffect, useState } from 'react';
interface DashboardData {
signups: any;
purchases: any;
activeUsers: any;
}
export default function AnalyticsDashboard() {
const [data, setData] = useState<DashboardData | null>(null);
useEffect(() => {
fetch('/api/dashboard')
.then(res => res.json())
.then(setData);
}, []);
if (!data) return <div>Loading analytics...</div>;
return (
<div className="grid grid-cols-3 gap-6">
<MetricCard
title="Signups (30d)"
value={sumEvents(data.signups)}
trend={calculateTrend(data.signups)}
/>
<MetricCard
title="Purchases (30d)"
value={sumEvents(data.purchases)}
trend={calculateTrend(data.purchases)}
/>
<MetricCard
title="Active Users (7d)"
value={sumEvents(data.activeUsers)}
trend={calculateTrend(data.activeUsers)}
/>
<div className="col-span-3">
<h3>Daily Signups</h3>
{/* Render chart with data.signups time series */}
</div>
</div>
);
}
function MetricCard({ title, value, trend }: {
title: string;
value: number;
trend: number;
}) {
return (
<div className="border rounded-lg p-6">
<p className="text-sm text-gray-500">{title}</p>
<p className="text-3xl font-bold">{value.toLocaleString()}</p>
<p className={trend >= 0 ? 'text-green-500' : 'text-red-500'}>
{trend >= 0 ? '↑' : '↓'} {Math.abs(trend)}%
</p>
</div>
);
}
5. Key Metrics to Track
| Metric | Mixpanel Query | Business Value |
|---|---|---|
| DAU / MAU | Unique "Login" events | User engagement |
| Signup conversion | Funnel: Visit → Signup | Acquisition efficiency |
| Activation rate | Funnel: Signup → First Value Action | Onboarding quality |
| Revenue per user | "Purchase" events aggregated | Monetization |
| Retention (Day 1/7/30) | Retention report on "Login" | Stickiness |
| Feature adoption | "Feature Used" by feature_name | Product-market fit |
| Churn rate | Users inactive for 30+ days | Retention health |
Pricing
| Plan | Events/Month | Price |
|---|---|---|
| Free | 20M | $0 |
| Growth | 100M+ | From $20/month |
| Enterprise | Custom | Custom |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Tracking too many events | Noise, hard to find insights | Track 10-15 key events max |
| Not identifying users | Can't track across sessions | Call identify() after login |
| Events without properties | Useless data | Always add context (plan, source, etc.) |
| Client-only tracking | Missed server events (purchases) | Use server-side for critical events |
| No naming convention | "signup", "Sign_Up", "user_signup" | Pick a convention and enforce it |
Making Dashboards Actually Real-Time
The dashboard pattern above fetches data when the page loads, which gives you a snapshot, not a live feed. True real-time requires a different approach: either polling or SSE push.
Polling: Set up a setInterval in the dashboard component to refetch the /api/dashboard route every 30-60 seconds. Simple, works everywhere, and sufficient for most analytics use cases — analytics data that updates every 30 seconds feels real-time to end users. Cache the Mixpanel API responses server-side with a short TTL (15-30 seconds) so that multiple dashboard viewers don't each trigger separate Mixpanel API calls.
SSE (Server-Sent Events): For true streaming event feeds (e.g., showing incoming events as they happen), set up an SSE endpoint that polls Mixpanel's live event export API and forwards events to connected clients. This is more complex and Mixpanel's query API has rate limits, so it's only worth building for operational dashboards (monitoring live system health) rather than product analytics dashboards.
Client-side vs. server-side rendering: The dashboard component above is fully client-side, which means the user sees a loading state before data appears. For internal dashboards where time-to-interactive matters less, this is fine. For public-facing analytics embeds or dashboards that appear in marketing materials, server-side render the initial data and hydrate the trend charts on the client.
Charting libraries for analytics dashboards: Recharts (React-native, 330KB) and Chart.js (lightweight, good for simple line/bar charts) are the most common choices. For more complex analytics visualizations like retention heatmaps or cohort grids, Tremor's React component library provides pre-built analytics-specific charts that work well with Mixpanel's data format. Avoid embedding Mixpanel's own embedded reports in iframes — they don't inherit your app's authentication and create a jarring UX context switch. Build the charts natively with your charting library and the Data Export API data.
Data caching strategy: Mixpanel API calls take 500ms-2s depending on date range and query complexity. Cache responses in Redis or your database with a TTL matched to the data freshness you need. For a 30-day historical dashboard, cache for 1 hour. For a "last 7 days" operational view, cache for 5-10 minutes. For a real-time event counter, don't cache the Mixpanel query — instead, maintain your own counter in Redis that you increment on each tracked event and only use Mixpanel for historical analysis.
Embedding in customer-facing pages: If you want to show customers their own usage data (e.g., "Your team made 847 API calls this month"), use Mixpanel's distinct_id filtering in your queries to scope results to the authenticated user. Always scope queries by distinct_id or user property filters server-side — never pass the full dataset to the client and filter there, as that leaks other users' data.
Mixpanel vs PostHog: Which to Use
Mixpanel and PostHog address the same core need — product analytics — but target different use cases and philosophies.
Choose Mixpanel when: You need best-in-class funnel and cohort analysis, your team is analytics-heavy and will use Mixpanel's built-in reports extensively, or you're already in the Mixpanel ecosystem and don't want to migrate.
Choose PostHog when: You want session recordings alongside events, you value open-source and self-hosting options, you need feature flags tightly integrated with your analytics, or you're a startup that wants to consolidate multiple tools (PostHog replaces Hotjar, LaunchDarkly, and Mixpanel to some extent).
Cost at scale: Mixpanel's free tier (20M events/month) is generous, but the jump to paid can be expensive at enterprise scale. PostHog's free tier (1M events/month) is smaller but the paid tier scales more predictably. Both are priced per event volume, so estimate your monthly event count before committing.
For the SaaS backend use case (sign up, login, feature use, purchase tracking), both tools are excellent. If you're already using Clerk + Stripe, PostHog integrates well with both and offers a tighter feedback loop between feature flag rollouts and analytics. If your growth team lives in a dedicated analytics tool and runs sophisticated SQL-like queries on cohorts, Mixpanel's query depth is superior.
Amplitude is the third major player, positioned between Mixpanel and enterprise BI tools. Amplitude's Behavioral Graph is uniquely powerful for understanding user journeys across many sessions, and its integration with Snowflake/BigQuery for raw data export makes it a better fit when your analytics team wants to run custom SQL alongside product analytics. Amplitude is typically more expensive than Mixpanel at comparable volume.
Segment as a middle layer: if you're uncertain about your long-term analytics tool choice, route events through Segment first. Segment fans out your events to multiple downstream tools simultaneously. You can start with Mixpanel, add PostHog for session recordings, and A/B test analytics tools — all without changing your instrumentation code. The trade-off is adding another vendor and another layer of complexity; it's worth it if you're genuinely evaluating multiple analytics tools, overkill if you've already committed.
Methodology
Mixpanel's Data Export API (used in the queryMixpanel helper) requires a service account, available in Mixpanel project settings under Access Security. The Basic authentication header uses the service account username and secret, not the project token. The Mixpanel ingestion SDK (mixpanel npm package) uses the project token, not the service account — do not confuse the two. Pricing data is from Mixpanel's public pricing page as of early 2026; 20M free events/month is the current free tier. The Mixpanel query API has rate limits (typically 60 requests/minute per project); the dashboard route should cache responses or implement deduplication to avoid hitting limits when multiple users view the dashboard simultaneously. mixpanel-browser v2.x and mixpanel (server) v0.5.x are the current npm packages.
Choosing analytics? Compare Mixpanel vs PostHog vs Amplitude on APIScout — pricing, features, and which fits your stack.
Related: Building a SaaS Backend, How to Add Product Analytics with PostHog, Set Up Segment for Customer Data in 2026