Skip to main content
RapidDev - Software Development Agency

How to Build KPI dashboard with V0

Build an executive KPI dashboard with V0 using Next.js, Supabase, and Recharts. Track revenue, churn, MRR, and custom metrics with goal lines, trend indicators, and sparkline charts — all rendered server-side for zero-JS initial load. Takes about 1-2 hours to complete.

What you'll build

  • Dashboard grid with large metric Cards showing current values, trend arrows, and sparkline charts
  • KPI deep-dive pages with historical Recharts AreaChart and goal reference lines
  • Goal tracking with Progress bars showing completion percentage toward targets
  • Time range switching with Tabs (7-day, 30-day, 90-day views)
  • Scheduled KPI refresh via Vercel Cron Jobs pulling latest values from data sources
  • Supabase RPC function computing trend direction and percentage change server-side
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 an executive KPI dashboard with V0 using Next.js, Supabase, and Recharts. Track revenue, churn, MRR, and custom metrics with goal lines, trend indicators, and sparkline charts — all rendered server-side for zero-JS initial load. Takes about 1-2 hours to complete.

What you're building

Startup founders and ops leads need a single view of their most important numbers — revenue, churn rate, MRR, active users — with goal lines and trend indicators. Most dashboard tools cost hundreds per month and require complex setup. You can build one tailored to your metrics in V0.

V0 generates the Card-based dashboard layout, Recharts visualizations, and Supabase queries from natural language prompts. Use the Connect panel for instant database setup and Design Mode to visually adjust card sizes and typography without spending credits.

The architecture uses Server Components for the dashboard grid (zero JavaScript on initial load), a Supabase RPC function for trend calculations, Vercel Cron Jobs for scheduled data refresh, and client components only for interactive charts and time range switching.

Final result

An executive KPI dashboard with metric cards, trend sparklines, goal tracking, historical charts, and automated data refresh via Vercel Cron Jobs.

Tech stack

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

Prerequisites

  • A V0 account (Premium or higher recommended)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • Your key metrics defined (e.g., MRR, churn rate, active users, revenue)
  • Data source for your KPIs (existing Supabase tables, external APIs, or manual entry)

Build steps

1

Set up the database schema for KPIs and history

Create the Supabase schema for storing KPI definitions, historical values, and goals. Each KPI has a direction (up = good, down = good) and a target value for goal tracking.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a KPI dashboard system. Create a Supabase schema with these tables:
3// 1. kpis: id (uuid PK), name (text), slug (text UNIQUE), unit (text like '$', '%', '#'), direction (text CHECK IN 'up','down'), current_value (numeric), target_value (numeric), updated_at (timestamptz)
4// 2. kpi_history: id (uuid PK), kpi_id (uuid FK to kpis), value (numeric), period_start (date), period_end (date)
5// 3. goals: id (uuid PK), kpi_id (uuid FK to kpis), target (numeric), deadline (date), status (text DEFAULT 'active')
6// 4. data_sources: id (uuid PK), name (text), query_template (text), connection_config (jsonb)
7// Add RLS policies for authenticated users. Generate SQL and TypeScript types.

Pro tip: Use V0's Connect panel to wire up Supabase in one click. The database URL and anon key are auto-populated in the Vars tab.

Expected result: Supabase is connected with all four tables created. KPI definitions, history, goals, and data source configurations are ready.

2

Build the main dashboard grid with metric cards

Create the dashboard page showing all KPIs as Cards with large numbers, trend arrows, sparkline charts, and goal progress. This page uses Server Components for fast initial load — the data is fetched server-side with zero client JavaScript.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a KPI dashboard at app/kpis/page.tsx.
3// Requirements:
4// - Fetch all KPIs from Supabase with their latest 7 history values
5// - Display as a responsive grid of shadcn/ui Cards (3 columns on desktop, 1 on mobile)
6// - Each Card shows: KPI name, current_value in large text with unit, trend arrow (up green or down red based on direction), percentage change from previous period
7// - Add a tiny Recharts sparkline (AreaChart, 80px tall) inside each Card showing the last 7 data points
8// - Below the value, show a Progress bar toward the target_value
9// - Add Tabs at the top for time range: 7d, 30d, 90d
10// - Add a Tooltip on hover showing the exact value and date for each sparkline point
11// - Use Server Components for the page, wrap sparklines in a 'use client' component

Pro tip: Use V0's Design Mode (Option+D) to adjust card sizes, reorder the grid, and tweak the typography of metric values. Visual adjustments are free — no credits spent.

Expected result: The dashboard shows a grid of KPI Cards with large numbers, trend indicators, sparkline charts, and goal progress bars.

3

Create the KPI deep-dive page with historical charts

Build a detail page for each KPI showing a full historical chart, goal reference line, and period comparison. Use dynamic routes with the KPI slug for clean URLs.

components/kpi-chart.tsx
1'use client'
2
3import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine, ResponsiveContainer } from 'recharts'
4import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
5import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
6
7interface KpiChartProps {
8 data: { date: string; value: number }[]
9 target: number
10 unit: string
11 direction: 'up' | 'down'
12}
13
14export function KpiChart({ data, target, unit, direction }: KpiChartProps) {
15 const color = direction === 'up' ? '#22c55e' : '#ef4444'
16
17 return (
18 <Card>
19 <CardHeader>
20 <CardTitle>Historical Trend</CardTitle>
21 </CardHeader>
22 <CardContent>
23 <Tabs defaultValue="30d">
24 <TabsList>
25 <TabsTrigger value="7d">7 Days</TabsTrigger>
26 <TabsTrigger value="30d">30 Days</TabsTrigger>
27 <TabsTrigger value="90d">90 Days</TabsTrigger>
28 </TabsList>
29 <TabsContent value="30d">
30 <ResponsiveContainer width="100%" height={400}>
31 <AreaChart data={data}>
32 <CartesianGrid strokeDasharray="3 3" />
33 <XAxis dataKey="date" />
34 <YAxis tickFormatter={(v) => `${unit}${v}`} />
35 <Tooltip formatter={(v: number) => [`${unit}${v}`, 'Value']} />
36 <ReferenceLine y={target} stroke="#6366f1" strokeDasharray="5 5" label="Goal" />
37 <Area type="monotone" dataKey="value" stroke={color} fill={color} fillOpacity={0.15} />
38 </AreaChart>
39 </ResponsiveContainer>
40 </TabsContent>
41 </Tabs>
42 </CardContent>
43 </Card>
44 )
45}

Expected result: The deep-dive page shows a full historical AreaChart with a dashed goal reference line, time range tabs, and formatted tooltips.

4

Build the trend calculation RPC function

Create a Supabase database function that computes trend direction and percentage change by comparing the current period to the previous period. This runs server-side via RPC for zero-JS initial load on the dashboard.

supabase/migrations/kpi_trends.sql
1-- Run this in Supabase SQL Editor
2CREATE OR REPLACE FUNCTION get_kpi_trends(p_days int DEFAULT 30)
3RETURNS TABLE (
4 kpi_id uuid,
5 kpi_name text,
6 kpi_slug text,
7 unit text,
8 direction text,
9 current_value numeric,
10 previous_value numeric,
11 target_value numeric,
12 delta numeric,
13 delta_pct numeric,
14 trend text
15) AS $$
16BEGIN
17 RETURN QUERY
18 SELECT
19 k.id AS kpi_id,
20 k.name AS kpi_name,
21 k.slug AS kpi_slug,
22 k.unit,
23 k.direction,
24 k.current_value,
25 COALESCE(
26 (SELECT h.value FROM kpi_history h
27 WHERE h.kpi_id = k.id
28 ORDER BY h.period_end DESC OFFSET 1 LIMIT 1),
29 k.current_value
30 ) AS previous_value,
31 k.target_value,
32 k.current_value - COALESCE(
33 (SELECT h.value FROM kpi_history h
34 WHERE h.kpi_id = k.id
35 ORDER BY h.period_end DESC OFFSET 1 LIMIT 1),
36 k.current_value
37 ) AS delta,
38 CASE
39 WHEN COALESCE(
40 (SELECT h.value FROM kpi_history h
41 WHERE h.kpi_id = k.id
42 ORDER BY h.period_end DESC OFFSET 1 LIMIT 1),
43 0
44 ) = 0 THEN 0
45 ELSE ROUND(
46 ((k.current_value - (SELECT h.value FROM kpi_history h
47 WHERE h.kpi_id = k.id
48 ORDER BY h.period_end DESC OFFSET 1 LIMIT 1)) /
49 (SELECT h.value FROM kpi_history h
50 WHERE h.kpi_id = k.id
51 ORDER BY h.period_end DESC OFFSET 1 LIMIT 1)) * 100, 1
52 )
53 END AS delta_pct,
54 CASE
55 WHEN k.current_value > COALESCE(
56 (SELECT h.value FROM kpi_history h
57 WHERE h.kpi_id = k.id
58 ORDER BY h.period_end DESC OFFSET 1 LIMIT 1),
59 k.current_value
60 ) THEN 'up'
61 ELSE 'down'
62 END AS trend
63 FROM kpis k;
64END;
65$$ LANGUAGE plpgsql SECURITY DEFINER;

Expected result: The RPC function returns all KPIs with their trend direction, delta, and percentage change. Call it from Server Components with supabase.rpc('get_kpi_trends').

5

Set up KPI management and goal CRUD

Build the settings page where users can add, edit, and delete KPI definitions and set goals. Use Server Actions for mutations and react-hook-form with zod for validation.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a KPI settings page at app/kpis/settings/page.tsx.
3// Requirements:
4// - List all KPI definitions in a shadcn/ui Table with columns: Name, Slug, Unit, Direction, Current Value, Target, Actions
5// - Add a 'New KPI' Button that opens a Dialog with form fields:
6// - name (Input), slug (Input, auto-generated from name), unit (Select: $, %, #, custom)
7// - direction (RadioGroup: 'Higher is better' / 'Lower is better'), target_value (Input number)
8// - Edit button on each row opens the same Dialog pre-filled
9// - Delete button with AlertDialog confirmation
10// - Goals section below: Table of active goals with KPI name, target, deadline, Progress bar
11// - Add Goal button: Select KPI, target number Input, deadline Calendar date picker
12// - Use Server Actions for all CRUD operations (insert, update, delete on kpis and goals tables)

Pro tip: Use zod validation in your Server Actions to ensure KPI slugs are URL-safe and target values are positive numbers before inserting into Supabase.

Expected result: The settings page allows full CRUD on KPI definitions and goals with form validation and confirmation dialogs.

6

Add scheduled KPI refresh and deploy

Set up a Vercel Cron Job that periodically fetches the latest KPI values from your data sources and updates the dashboard. Configure the cron endpoint with secret authentication and deploy.

app/api/kpis/refresh/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function GET(req: NextRequest) {
10 const authHeader = req.headers.get('Authorization')
11 if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
12 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
13 }
14
15 const { data: sources } = await supabase
16 .from('data_sources')
17 .select('*, kpis(id, slug)')
18
19 const updates: { kpi_id: string; value: number }[] = []
20
21 for (const source of sources || []) {
22 try {
23 // Execute the query template to fetch latest value
24 const { data } = await supabase.rpc('execute_kpi_query', {
25 p_query: source.query_template,
26 })
27 if (data?.value !== undefined) {
28 updates.push({ kpi_id: source.kpis.id, value: data.value })
29 }
30 } catch (err) {
31 console.error(`Failed to refresh ${source.name}:`, err)
32 }
33 }
34
35 for (const update of updates) {
36 await supabase
37 .from('kpis')
38 .update({ current_value: update.value, updated_at: new Date().toISOString() })
39 .eq('id', update.kpi_id)
40
41 await supabase.from('kpi_history').insert({
42 kpi_id: update.kpi_id,
43 value: update.value,
44 period_start: new Date().toISOString().split('T')[0],
45 period_end: new Date().toISOString().split('T')[0],
46 })
47 }
48
49 return NextResponse.json({ refreshed: updates.length })
50}

Expected result: The cron endpoint refreshes all KPI values from configured data sources. Deploy and configure Vercel Cron in vercel.json to run hourly or daily.

Complete code

app/api/kpis/refresh/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function GET(req: NextRequest) {
10 // Verify cron secret for security
11 const authHeader = req.headers.get('Authorization')
12 if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
13 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
14 }
15
16 // Fetch all KPIs with their current values
17 const { data: kpis } = await supabase
18 .from('kpis')
19 .select('id, slug, current_value')
20
21 const today = new Date().toISOString().split('T')[0]
22 let refreshed = 0
23
24 for (const kpi of kpis || []) {
25 // Record current value in history
26 await supabase.from('kpi_history').upsert(
27 {
28 kpi_id: kpi.id,
29 value: kpi.current_value,
30 period_start: today,
31 period_end: today,
32 },
33 { onConflict: 'kpi_id,period_start' }
34 )
35 refreshed++
36 }
37
38 // Update timestamp on all KPIs
39 await supabase
40 .from('kpis')
41 .update({ updated_at: new Date().toISOString() })
42 .not('id', 'is', null)
43
44 return NextResponse.json({
45 success: true,
46 refreshed,
47 timestamp: new Date().toISOString(),
48 })
49}

Customization ideas

Slack notifications for goal milestones

Send a Slack message via the Slack API when a KPI crosses its goal threshold, celebrating the win or alerting the team to a negative trend.

PDF report generation

Add a weekly PDF report endpoint using @react-pdf/renderer that captures the current dashboard state with charts and sends it via email.

External data source connectors

Pull KPI values directly from Stripe (MRR, revenue), Google Analytics (users, sessions), or your production database using scheduled API calls.

Team annotations and comments

Allow team members to add text annotations to specific data points on the charts, explaining spikes or dips for historical context.

Embeddable dashboard widgets

Create a public API endpoint that returns KPI Card HTML for embedding individual metrics in Notion, Slack, or other tools via iframe.

Common pitfalls

Pitfall: Calculating trend percentages in client-side JavaScript

How to avoid: Use a Supabase RPC function (get_kpi_trends) to compute deltas and percentages server-side. Call it from a Server Component for zero-JS initial load.

Pitfall: Not securing the cron refresh endpoint with a secret

How to avoid: Add a CRON_SECRET env var in V0's Vars tab and check the Authorization header in the refresh route. Vercel Cron Jobs automatically send this header.

Pitfall: Using NEXT_PUBLIC_ prefix for SUPABASE_SERVICE_ROLE_KEY

How to avoid: Store SUPABASE_SERVICE_ROLE_KEY without any prefix in the Vars tab. Use it only in API routes and Server Components.

Best practices

  • Use Server Components for the dashboard grid — KPI data loads server-side with zero client JavaScript for instant first paint
  • Wrap Recharts charts in 'use client' components and keep the parent dashboard page as a Server Component that passes data as props
  • Use V0's Design Mode (Option+D) to adjust Card sizes, reorder the dashboard grid, and tweak metric typography without spending credits
  • Configure Vercel Cron Jobs in vercel.json for scheduled KPI refresh: {"crons": [{"path": "/api/kpis/refresh", "schedule": "0 * * * *"}]}
  • Store KPI history with period_start and period_end dates for flexible time-range queries and accurate trend calculations
  • Use Supabase RPC functions for complex calculations like trend direction and delta percentage to avoid redundant logic in multiple components
  • Add a CRON_SECRET environment variable and validate it in the refresh endpoint to prevent unauthorized access
  • Use Recharts ReferenceLine component to show goal lines on charts — it is a built-in feature that requires no custom drawing code

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a KPI dashboard with Next.js App Router and Supabase. I need a PostgreSQL function called get_kpi_trends that returns all KPIs with their current value, previous period value, delta, percentage change, and trend direction (up/down). The function should compare the most recent kpi_history entry to the one before it. Tables: kpis (id, name, slug, unit, direction, current_value, target_value), kpi_history (kpi_id, value, period_start, period_end). Please write the SQL function.

Build Prompt

Create a KPI card component that displays: large metric value with unit prefix, trend arrow icon (ArrowUp or ArrowDown from lucide-react), percentage change Badge (green for positive, red for negative), a Progress bar showing progress toward target, and a tiny Recharts AreaChart sparkline (80px tall, no axes, just the line). The card should accept props: name, value, unit, delta_pct, target, direction, history (array of {date, value}). Make it a Server Component with only the sparkline as a nested 'use client' component.

Frequently asked questions

How do I connect my existing data sources to the KPI dashboard?

The data_sources table stores query templates and connection configs. The scheduled refresh endpoint executes these queries via Supabase RPC and updates KPI values. For external services like Stripe or Google Analytics, add API calls in the refresh route using their respective SDKs.

Can the dashboard update in real-time?

Yes. Use Supabase Realtime to subscribe to changes on the kpis table. When the cron job updates a KPI value, the Realtime subscription pushes the new value to connected clients instantly. Wrap the subscription in a useEffect inside a 'use client' component.

Do I need a paid V0 plan?

Premium ($20/month) is recommended. The KPI dashboard has multiple pages (main grid, deep-dive, settings) and chart components that require several prompts. The free tier's limited credits may run out before you finish.

How does the scheduled refresh work?

Add a crons configuration in vercel.json that hits your /api/kpis/refresh endpoint on a schedule (e.g., hourly). The endpoint is secured with a CRON_SECRET that Vercel sends automatically. It fetches the latest values and updates both the kpis table and kpi_history.

Can I add custom KPIs without changing code?

Yes. The settings page lets you create new KPI definitions with a name, unit, direction, and target. Once added, the KPI automatically appears on the dashboard and can be updated manually or via the refresh endpoint.

How do I deploy the KPI dashboard?

Click Share in V0, then Publish to Production. Add a vercel.json file with the crons configuration for scheduled refresh. Set SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and CRON_SECRET in V0's Vars tab before deploying.

Can RapidDev help build a custom KPI dashboard?

Yes. RapidDev has built over 600 apps including executive dashboards with real-time data connectors, automated reporting, and Slack integrations. Book a free consultation to discuss your metrics and data sources.

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.