Skip to main content
RapidDev - Software Development Agency

How to Build Product analytics with V0

Build a product analytics dashboard with V0 using Next.js and Supabase that tracks user behavior events, visualizes conversion funnels, and measures retention cohorts. Features an event ingestion API, Recharts-powered visualizations, and materialized views for fast aggregations — all in about 1-2 hours.

What you'll build

  • Event ingestion API route with project-based API key authentication
  • Overview dashboard with key metric Cards showing active users, events, and session counts
  • Conversion funnel builder and visualization using Recharts bar charts
  • Retention cohort heatmap displaying week-over-week user retention
  • Time-range selector with shadcn/ui Tabs for 24h, 7d, 30d, and 90d periods
  • Materialized views with pg_cron for pre-computed daily metrics at scale
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate12 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a product analytics dashboard with V0 using Next.js and Supabase that tracks user behavior events, visualizes conversion funnels, and measures retention cohorts. Features an event ingestion API, Recharts-powered visualizations, and materialized views for fast aggregations — all in about 1-2 hours.

What you're building

Understanding how users interact with your product is essential for growth. Product analytics helps you identify where users drop off, which features drive engagement, and how well you retain users over time. Tools like Mixpanel and Amplitude charge hundreds per month — you can build your own.

V0 makes this practical by generating the Next.js dashboard pages, API routes, and chart components from prompts. Recharts is included in every V0 project, so you get professional data visualizations without installing anything. Connect Supabase via the Connect panel for your analytics database.

The architecture uses Next.js App Router with an Edge-optimized event ingestion endpoint, Server Components for the dashboard, Recharts for line charts and funnel visualizations, and Supabase with materialized views for fast metric aggregation.

Final result

A self-hosted product analytics platform with event ingestion, funnel analysis, retention cohorts, and a real-time dashboard with Recharts visualizations — all running on your own infrastructure.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
RechartsData Visualization

Prerequisites

  • A V0 account (Premium recommended for dashboard complexity)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • Basic understanding of product analytics concepts (events, funnels, retention)
  • A product or website to track events from

Build steps

1

Set up the project and analytics schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Then prompt V0 to create the full schema for events, projects, funnels, and dashboards with proper indexes for query performance.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a product analytics platform. Create a Supabase schema with:
3// 1. projects: id (uuid PK), owner_id (uuid FK), name (text), api_key (uuid default gen_random_uuid() unique), created_at (timestamptz)
4// 2. events: id (uuid PK), project_id (uuid FK), user_id (text), session_id (text), event_name (text), properties (jsonb), page_url (text), referrer (text), device_info (jsonb), created_at (timestamptz)
5// Add index on events (project_id, event_name, created_at)
6// 3. funnels: id (uuid PK), project_id (uuid FK), name (text), steps (jsonb), created_at (timestamptz)
7// 4. dashboards: id (uuid PK), project_id (uuid FK), name (text), widgets (jsonb), created_at (timestamptz)
8// Add RLS: project owners can read their own data.
9// Generate SQL migration and TypeScript types.

Pro tip: Use V0's Connect panel for one-click Supabase provisioning — analytics requires a real database from the start, so set this up before building any UI.

Expected result: Supabase is connected with events, projects, funnels, and dashboards tables created. The events table has an index for fast queries by project, event name, and date.

2

Build the event ingestion API endpoint

Create an Edge Runtime API route that accepts event batches from tracked websites. The endpoint validates the project API key, processes the event payload, and inserts into the events table. Edge Runtime ensures global low-latency ingestion.

app/api/events/ingest/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4export const runtime = 'edge'
5
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function POST(req: NextRequest) {
12 const apiKey = req.headers.get('x-api-key')
13 if (!apiKey) {
14 return NextResponse.json({ error: 'Missing API key' }, { status: 401 })
15 }
16
17 const { data: project } = await supabase
18 .from('projects')
19 .select('id')
20 .eq('api_key', apiKey)
21 .single()
22
23 if (!project) {
24 return NextResponse.json({ error: 'Invalid API key' }, { status: 401 })
25 }
26
27 const { events } = await req.json()
28 if (!Array.isArray(events) || events.length === 0) {
29 return NextResponse.json({ error: 'Events array required' }, { status: 400 })
30 }
31
32 const rows = events.map((e: Record<string, unknown>) => ({
33 project_id: project.id,
34 user_id: e.user_id as string,
35 session_id: e.session_id as string,
36 event_name: e.event_name as string,
37 properties: e.properties ?? {},
38 page_url: e.page_url as string,
39 referrer: e.referrer as string,
40 device_info: e.device_info ?? {},
41 }))
42
43 const { error } = await supabase.from('events').insert(rows)
44 if (error) {
45 return NextResponse.json({ error: error.message }, { status: 500 })
46 }
47
48 return NextResponse.json({ ingested: rows.length })
49}

Expected result: POST /api/events/ingest accepts a batch of events with an x-api-key header. Events are validated and inserted into Supabase. The Edge Runtime ensures low latency globally.

3

Build the analytics overview dashboard

Create the main dashboard page showing key metrics: active users, total events, and sessions for the selected time range. Use Recharts for a line chart showing event volume over time, and shadcn/ui Cards for metric summaries.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a product analytics dashboard at app/dashboard/page.tsx.
3// Requirements:
4// - Fetch event counts from Supabase for the selected project
5// - Show 4 metric Cards at the top: Active Users, Total Events, Sessions, Events/User
6// - Each Card shows the current value and percentage change from previous period
7// - Add shadcn/ui Tabs for time range selection: 24h, 7d, 30d, 90d
8// - Below metrics, show a Recharts LineChart of events over time (x-axis: date, y-axis: event count)
9// - Add a Table below the chart showing top 10 event names with count and percentage
10// - Use HoverCard on metric Cards to show tooltip with additional context
11// - Use Select for filtering by event_name
12// - Server Components for data fetching
13// - Use Skeleton components while data loads

Expected result: A dashboard with metric Cards, time range Tabs, a Recharts LineChart showing event trends, and a Table of top events — all filtered by the selected time range.

4

Create the funnel builder and visualization

Build a funnel analysis page where users define conversion steps (e.g., page_view > sign_up > purchase) and see how many users complete each step. Use Recharts horizontal bar chart for the funnel visualization.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a funnel analysis page at app/dashboard/funnels/page.tsx.
3// Requirements:
4// - Show existing funnels in a list with name and step count
5// - "Create Funnel" Button opens a Dialog with:
6// - Input for funnel name
7// - Dynamic step builder: each step has a Select for event_name (populated from distinct event names)
8// - Add Step and Remove Step buttons
9// - Save via Server Action createFunnel()
10// - When a funnel is selected, show visualization:
11// - Recharts BarChart (horizontal) with each step as a bar
12// - Bar width represents percentage of users who reached that step
13// - Labels show: step name, user count, conversion rate from previous step
14// - Use Badge for conversion rate (green >50%, yellow 20-50%, red <20%)
15// - Funnel data is computed by querying events table:
16// - Step 1: count distinct user_ids with event_name = step1
17// - Step 2: count distinct user_ids from step 1 set who also have event_name = step2 (after step1 timestamp)
18// - Continue for each step
19// - Server Components for data fetching, client component for interactive chart

Pro tip: For large event volumes, create a Supabase database function that computes the funnel server-side rather than fetching all events to the application layer.

Expected result: A funnel analysis page where you define conversion steps and visualize drop-off rates between each step as horizontal bars with conversion percentage labels.

5

Add materialized views for fast aggregations

As your event volume grows, real-time COUNT queries get slow. Create materialized views that pre-compute daily and weekly metrics, refreshed automatically by pg_cron. This keeps your dashboard responsive even with millions of events.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a Supabase SQL migration for analytics materialized views:
3// 1. A materialized view daily_metrics AS:
4// SELECT project_id, date_trunc('day', created_at) as date,
5// event_name, count(*) as event_count,
6// count(distinct user_id) as unique_users,
7// count(distinct session_id) as sessions
8// FROM events GROUP BY project_id, date, event_name
9// 2. A materialized view retention_cohorts AS:
10// SELECT project_id,
11// date_trunc('week', first_seen) as cohort_week,
12// date_trunc('week', created_at) as activity_week,
13// count(distinct user_id) as users
14// FROM events JOIN (SELECT project_id, user_id, min(created_at) as first_seen FROM events GROUP BY project_id, user_id) first_events USING (project_id, user_id)
15// GROUP BY project_id, cohort_week, activity_week
16// 3. A pg_cron job that refreshes both views every hour: SELECT cron.schedule('refresh-analytics', '0 * * * *', 'REFRESH MATERIALIZED VIEW CONCURRENTLY daily_metrics; REFRESH MATERIALIZED VIEW CONCURRENTLY retention_cohorts;')
17// 4. Update the dashboard to query from daily_metrics instead of events table
18// Also create app/dashboard/retention/page.tsx showing a cohort heatmap using Recharts.

Expected result: Materialized views are created and auto-refreshed hourly. The dashboard queries pre-computed metrics instead of scanning raw events, keeping response times fast even at scale.

6

Build the retention cohort heatmap

Create a retention analysis page showing a cohort heatmap where rows represent user cohorts (by signup week) and columns show retention in subsequent weeks. Use color intensity to indicate retention rates.

app/dashboard/retention/page.tsx
1import { createClient } from '@/lib/supabase/server'
2import { Card } from '@/components/ui/card'
3import { Badge } from '@/components/ui/badge'
4
5export default async function RetentionPage() {
6 const supabase = await createClient()
7
8 const { data: cohorts } = await supabase
9 .from('retention_cohorts')
10 .select('cohort_week, activity_week, users')
11 .order('cohort_week', { ascending: false })
12 .limit(500)
13
14 const cohortMap = new Map<string, Map<number, number>>()
15 const cohortSizes = new Map<string, number>()
16
17 cohorts?.forEach((row) => {
18 const key = row.cohort_week as string
19 const weekDiff = Math.round(
20 (new Date(row.activity_week as string).getTime() -
21 new Date(key).getTime()) /
22 (7 * 24 * 60 * 60 * 1000)
23 )
24 if (!cohortMap.has(key)) cohortMap.set(key, new Map())
25 cohortMap.get(key)!.set(weekDiff, row.users as number)
26 if (weekDiff === 0) cohortSizes.set(key, row.users as number)
27 })
28
29 return (
30 <div className="p-6">
31 <h1 className="text-2xl font-bold mb-6">Retention Cohorts</h1>
32 <Card className="p-4 overflow-x-auto">
33 <table className="w-full text-sm">
34 <thead>
35 <tr>
36 <th className="text-left p-2">Cohort</th>
37 <th className="p-2">Size</th>
38 {[...Array(8)].map((_, i) => (
39 <th key={i} className="p-2">W{i}</th>
40 ))}
41 </tr>
42 </thead>
43 <tbody>
44 {[...cohortMap.entries()].slice(0, 12).map(([week, data]) => {
45 const size = cohortSizes.get(week) ?? 1
46 return (
47 <tr key={week}>
48 <td className="p-2 font-medium">{week.slice(0, 10)}</td>
49 <td className="p-2 text-center">
50 <Badge variant="outline">{size}</Badge>
51 </td>
52 {[...Array(8)].map((_, i) => {
53 const users = data.get(i) ?? 0
54 const pct = Math.round((users / size) * 100)
55 const opacity = pct / 100
56 return (
57 <td
58 key={i}
59 className="p-2 text-center text-xs"
60 style={{ backgroundColor: `rgba(59, 130, 246, ${opacity})`, color: pct > 50 ? 'white' : 'inherit' }}
61 >
62 {pct}%
63 </td>
64 )
65 })}
66 </tr>
67 )
68 })}
69 </tbody>
70 </table>
71 </Card>
72 </div>
73 )
74}

Expected result: A color-coded cohort heatmap showing retention rates by week. Each cell shows the percentage of users from that cohort still active in week N, with darker colors for higher retention.

Complete code

app/api/events/ingest/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4export const runtime = 'edge'
5
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function POST(req: NextRequest) {
12 const apiKey = req.headers.get('x-api-key')
13 if (!apiKey) {
14 return NextResponse.json({ error: 'Missing API key' }, { status: 401 })
15 }
16
17 const { data: project } = await supabase
18 .from('projects')
19 .select('id')
20 .eq('api_key', apiKey)
21 .single()
22
23 if (!project) {
24 return NextResponse.json({ error: 'Invalid API key' }, { status: 401 })
25 }
26
27 const { events } = await req.json()
28 if (!Array.isArray(events) || events.length === 0) {
29 return NextResponse.json({ error: 'Events required' }, { status: 400 })
30 }
31
32 const rows = events.slice(0, 100).map((e: Record<string, unknown>) => ({
33 project_id: project.id,
34 user_id: e.user_id as string,
35 session_id: e.session_id as string,
36 event_name: e.event_name as string,
37 properties: e.properties ?? {},
38 page_url: e.page_url as string,
39 referrer: e.referrer as string,
40 device_info: e.device_info ?? {},
41 }))
42
43 const { error } = await supabase.from('events').insert(rows)
44 if (error) {
45 return NextResponse.json({ error: error.message }, { status: 500 })
46 }
47
48 return NextResponse.json({ ingested: rows.length })
49}

Customization ideas

Add real-time event stream

Use Supabase Realtime to subscribe to new events and display a live feed of user actions on the dashboard as they happen.

Build custom dashboard widgets

Let users create saved dashboards with drag-and-drop widgets for different metrics, chart types, and filter configurations stored in the dashboards table.

Add user journey mapping

Visualize the sequence of events for individual users as a timeline, showing the exact path they took through your product.

Integrate anomaly detection

Use statistical analysis to automatically detect unusual spikes or drops in key metrics and trigger alerts when anomalies are found.

Common pitfalls

Pitfall: Querying the raw events table for dashboard metrics on every page load

How to avoid: Use Supabase materialized views refreshed by pg_cron to pre-compute daily and weekly aggregations. Query the materialized view instead of the events table.

Pitfall: Not using connection pooling for the ingestion endpoint

How to avoid: Use the Supavisor pooled connection string from Supabase project settings. Set this as SUPABASE_URL in the Vars tab.

Pitfall: Exposing the event ingestion endpoint without API key validation

How to avoid: Validate the x-api-key header against the projects table before processing events. Return 401 for missing or invalid keys.

Pitfall: Setting CORS headers too permissively on the ingestion endpoint

How to avoid: Set Access-Control-Allow-Origin to your specific domain(s), or validate the Origin header against your projects table.

Best practices

  • Use Edge Runtime for the event ingestion endpoint to minimize latency for tracked websites worldwide
  • Use V0's Connect panel for one-click Supabase provisioning — analytics requires a database from the start
  • Create materialized views with pg_cron for pre-computed metrics instead of real-time COUNT queries
  • Limit event batches to 100 items per request to prevent abuse and stay within serverless memory limits
  • Use Supavisor connection pooling to handle high-concurrency event ingestion without exhausting connections
  • Use Recharts (bundled with V0 projects) for all chart rendering — no additional packages needed
  • Index the events table on (project_id, event_name, created_at) for fast time-range queries
  • Use V0's Design Mode (Option+D) to visually adjust dashboard Card spacing and chart colors for free

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a product analytics dashboard with Next.js App Router and Supabase. Help me write a PostgreSQL function that computes a conversion funnel. Given an events table with user_id, event_name, and created_at, and a funnel definition as an array of event names, the function should return the count of distinct users who completed each step in order (step N must occur after step N-1 for the same user). Return the SQL function.

Build Prompt

Create a retention cohort heatmap component. Query a retention_cohorts materialized view that has cohort_week, activity_week, and users columns. Display as an HTML table where rows are cohort weeks, columns are weeks 0-8 since first activity, and cells show retention percentage with blue background color intensity proportional to the percentage. Include a Badge for cohort size.

Frequently asked questions

Can I use this instead of Mixpanel or Amplitude?

For basic event tracking, funnels, and retention analysis — yes. You get full control over your data and zero monthly fees beyond Supabase hosting. For advanced features like predictive analytics or behavioral cohorts, commercial tools have an edge.

How many events can Supabase handle?

Supabase free tier handles up to 500MB of data (roughly 2-5 million events depending on payload size). The Pro plan at $25/month gives you 8GB. For high-volume analytics, use materialized views and consider partitioning the events table by month.

Do I need a paid V0 plan?

V0 Free works for the basic build, but Premium ($20/month) is recommended because the dashboard has multiple complex pages (overview, funnels, retention) that benefit from prompt queuing.

How do I track events from my website?

Add a lightweight JavaScript snippet to your website that sends event batches to your /api/events/ingest endpoint with your project API key in the x-api-key header. Events are collected client-side and sent in batches every few seconds.

How do I deploy the analytics dashboard?

Click Share then Publish to Production in V0. Set SUPABASE_SERVICE_ROLE_KEY in the Vars tab without NEXT_PUBLIC_ prefix. The event ingestion endpoint will be available at your Vercel domain.

Can RapidDev help build a custom analytics platform?

Yes. RapidDev has built 600+ apps including custom analytics platforms with real-time dashboards, ML-powered anomaly detection, and white-label reporting. Book a free consultation to discuss your specific analytics needs.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.