How to Build a SaaS Analytics Dashboard (2026 Guide)
Every SaaS needs an analytics dashboard. Users expect to see their data visualized, and you need internal metrics to make decisions. Here's how to build one that actually ships.
Choose Your Approach
| Approach | Time | Best For |
|---|---|---|
| Embedded analytics tool | 1-2 days | Customer-facing dashboards |
| Charting library + custom | 1-2 weeks | Full control, branded experience |
| Admin template | 2-3 days | Internal dashboards |
Option 1: Embedded Analytics (Fastest)
If you need customer-facing analytics without building everything:
Metabase (embedded) — Open-source BI that embeds in your app via iframe or SDK. Connect to your database, build charts, embed them. Free self-hosted.
Cube.js — Headless BI that provides a semantic layer over your database. You build the UI; Cube handles queries, caching, and access control. Open-source.
Tremor — React component library specifically for dashboards. Beautiful charts and KPI cards. Free, works with any data source.
Option 2: Custom Build (Most Control)
Recommended Stack
Frontend:
- React + Next.js (or your existing framework)
- Recharts or Visx for charts (lightweight, composable)
- Tremor or shadcn/ui for dashboard UI components
- TanStack Table for data tables
- date-fns for date manipulation
Backend:
- Your existing API
- Tinybird or ClickHouse for analytics queries (fast aggregations)
- PostgreSQL for transactional data
- Redis for caching expensive queries
Real-time (if needed):
- Server-Sent Events (simplest)
- WebSocket via Soketi or Ably
- Supabase Realtime if on Supabase
Core Metrics to Display
For your users (customer-facing):
- Usage metrics (API calls, storage, active users)
- Performance data (response times, uptime)
- Activity feed (recent actions, events)
- Billing/usage vs. plan limits
For you (internal):
- MRR/ARR and growth rate
- Churn rate and retention cohorts
- Customer acquisition cost (CAC)
- Feature adoption rates
- API response times (P50, P95, P99)
Implementation Steps
Step 1: Define your data model
Decide what you're tracking and how. For most SaaS:
-- Events table (append-only, high volume)
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
user_id UUID NOT NULL,
org_id UUID,
event_type VARCHAR(100) NOT NULL,
properties JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create indexes for common queries
CREATE INDEX idx_events_user_time ON events (user_id, created_at DESC);
CREATE INDEX idx_events_org_time ON events (org_id, created_at DESC);
CREATE INDEX idx_events_type_time ON events (event_type, created_at DESC);
Step 2: Build the aggregation layer
Don't query raw events for every dashboard load. Pre-aggregate:
-- Daily aggregates (materialized view or cron job)
CREATE TABLE daily_metrics (
org_id UUID,
date DATE,
event_type VARCHAR(100),
count INTEGER,
unique_users INTEGER,
PRIMARY KEY (org_id, date, event_type)
);
Or use Tinybird — send events via API, define aggregations in SQL, get instant API endpoints for your dashboard.
Step 3: Build API endpoints
// /api/dashboard/metrics
export async function GET(req: Request) {
const { orgId } = auth(req);
const { from, to, granularity } = parseParams(req);
const metrics = await db.query(`
SELECT date, event_type, count, unique_users
FROM daily_metrics
WHERE org_id = $1 AND date BETWEEN $2 AND $3
ORDER BY date
`, [orgId, from, to]);
return Response.json(metrics);
}
Step 4: Build the UI
import { AreaChart, Card, Metric, Text } from "@tremor/react";
function Dashboard({ metrics }) {
return (
<div className="grid grid-cols-3 gap-4">
<Card>
<Text>Total Events</Text>
<Metric>{metrics.totalEvents.toLocaleString()}</Metric>
</Card>
<Card>
<Text>Active Users</Text>
<Metric>{metrics.activeUsers}</Metric>
</Card>
<Card>
<Text>API Calls</Text>
<Metric>{metrics.apiCalls.toLocaleString()}</Metric>
</Card>
<Card className="col-span-3">
<Text>Events Over Time</Text>
<AreaChart
data={metrics.timeSeries}
index="date"
categories={["events", "users"]}
/>
</Card>
</div>
);
}
Step 5: Add date range picker and filters
Every dashboard needs:
- Date range selector (last 7d, 30d, 90d, custom)
- Granularity toggle (hourly, daily, weekly, monthly)
- Filters (by event type, user, plan)
- Export (CSV download)
Step 6: Optimize performance
- Cache aggregated queries (Redis, 5-minute TTL)
- Use materialized views for common aggregations
- Paginate large data tables
- Lazy-load chart components
- Use
React.memofor chart components (prevent re-renders)
Charting Libraries Compared
| Library | Bundle Size | Customization | Learning Curve |
|---|---|---|---|
| Recharts | 45KB | Medium | Low |
| Visx | Varies | Full | High |
| Chart.js | 60KB | Medium | Low |
| Nivo | 80KB+ | High | Medium |
| Tremor | Includes charts | Medium (opinionated) | Low |
| Apache ECharts | 100KB+ | Full | Medium |
Recommendation: Tremor for the fastest time to beautiful dashboard. Recharts for more control with reasonable simplicity. Visx for complete customization.
Performance Tips
Query Performance
- Pre-aggregate. Never query raw events for dashboard display
- Use ClickHouse or Tinybird for analytics if PostgreSQL gets slow at scale
- Partition tables by date for time-series queries
- Cache aggressively — dashboard data doesn't need to be real-time for most use cases
Frontend Performance
- Virtualize large tables (TanStack Virtual)
- Lazy-load charts below the fold
- Use SWR or React Query with stale-while-revalidate caching
- Skeleton loading states — show the layout while data loads
- Debounce filter changes — don't re-query on every keystroke
Common Mistakes
- Querying raw events on every page load — pre-aggregate or use a caching layer
- Too many charts — focus on 5-7 key metrics, not 30 charts
- No date range picker — users always want to adjust the time window
- Ignoring mobile — dashboards need to work on tablets at minimum
- No empty states — new users with no data should see helpful onboarding, not blank charts
- Exposing internal metrics — be intentional about what users see vs. what's internal
FAQ
Should I use a BI tool or build custom?
Build custom for customer-facing dashboards (better UX, branded, integrated). Use Metabase/Grafana for internal dashboards (faster to set up, more flexible querying).
How do I handle real-time updates?
For most SaaS: refresh data every 30-60 seconds (polling or SWR revalidation). True real-time (WebSocket) only if your product requires it (monitoring, live collaboration).
What about mobile dashboards?
Use responsive grid layouts. On mobile, stack cards vertically. Consider simplifying charts (fewer data points, larger touch targets). Tremor components are responsive by default.
How much data should I retain?
Keep raw events for 90 days - 1 year. Keep daily aggregates indefinitely. Monthly aggregates for historical trends. This balances storage costs with analytical capability.
The Bottom Line
For most SaaS in 2026:
- Track events in PostgreSQL (or Tinybird for scale)
- Pre-aggregate into daily/weekly summaries
- Build UI with Tremor + Recharts (fastest to production)
- Cache aggressively with React Query + Redis
- Start with 5 metrics and add more based on user feedback
Ship a simple dashboard fast, then iterate based on what users actually look at.