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
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
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.
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.
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.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34export const runtime = 'edge'56const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export 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 }1617 const { data: project } = await supabase18 .from('projects')19 .select('id')20 .eq('api_key', apiKey)21 .single()2223 if (!project) {24 return NextResponse.json({ error: 'Invalid API key' }, { status: 401 })25 }2627 const { events } = await req.json()28 if (!Array.isArray(events) || events.length === 0) {29 return NextResponse.json({ error: 'Events array required' }, { status: 400 })30 }3132 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 }))4243 const { error } = await supabase.from('events').insert(rows)44 if (error) {45 return NextResponse.json({ error: error.message }, { status: 500 })46 }4748 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.
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.
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 project5// - Show 4 metric Cards at the top: Active Users, Total Events, Sessions, Events/User6// - Each Card shows the current value and percentage change from previous period7// - Add shadcn/ui Tabs for time range selection: 24h, 7d, 30d, 90d8// - 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 percentage10// - Use HoverCard on metric Cards to show tooltip with additional context11// - Use Select for filtering by event_name12// - Server Components for data fetching13// - Use Skeleton components while data loadsExpected 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.
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.
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 count5// - "Create Funnel" Button opens a Dialog with:6// - Input for funnel name7// - Dynamic step builder: each step has a Select for event_name (populated from distinct event names)8// - Add Step and Remove Step buttons9// - Save via Server Action createFunnel()10// - When a funnel is selected, show visualization:11// - Recharts BarChart (horizontal) with each step as a bar12// - Bar width represents percentage of users who reached that step13// - Labels show: step name, user count, conversion rate from previous step14// - 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 = step117// - Step 2: count distinct user_ids from step 1 set who also have event_name = step2 (after step1 timestamp)18// - Continue for each step19// - Server Components for data fetching, client component for interactive chartPro 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.
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.
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 sessions8// FROM events GROUP BY project_id, date, event_name9// 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 users14// 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_week16// 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 table18// 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.
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.
1import { createClient } from '@/lib/supabase/server'2import { Card } from '@/components/ui/card'3import { Badge } from '@/components/ui/badge'45export default async function RetentionPage() {6 const supabase = await createClient()78 const { data: cohorts } = await supabase9 .from('retention_cohorts')10 .select('cohort_week, activity_week, users')11 .order('cohort_week', { ascending: false })12 .limit(500)1314 const cohortMap = new Map<string, Map<number, number>>()15 const cohortSizes = new Map<string, number>()1617 cohorts?.forEach((row) => {18 const key = row.cohort_week as string19 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 })2829 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) ?? 146 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) ?? 054 const pct = Math.round((users / size) * 100)55 const opacity = pct / 10056 return (57 <td58 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
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34export const runtime = 'edge'56const supabase = createClient(7 process.env.SUPABASE_URL!,8 process.env.SUPABASE_SERVICE_ROLE_KEY!9)1011export 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 }1617 const { data: project } = await supabase18 .from('projects')19 .select('id')20 .eq('api_key', apiKey)21 .single()2223 if (!project) {24 return NextResponse.json({ error: 'Invalid API key' }, { status: 401 })25 }2627 const { events } = await req.json()28 if (!Array.isArray(events) || events.length === 0) {29 return NextResponse.json({ error: 'Events required' }, { status: 400 })30 }3132 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 }))4243 const { error } = await supabase.from('events').insert(rows)44 if (error) {45 return NextResponse.json({ error: error.message }, { status: 500 })46 }4748 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation