Build a real-time internal metrics dashboard with V0 using Next.js, Supabase Realtime for live-updating data, and Recharts for sparklines and charts. You'll create configurable metric widgets, an activity feed, and an API endpoint for external metric ingestion — all in about 1-2 hours without touching a terminal.
What you're building
Non-technical founders need a single pane of glass showing team KPIs, recent activity, and system health without paying for Retool or Datadog. A custom internal dashboard provides exactly this with real-time updates.
V0 generates the metric cards, chart components, and activity feed from prompts. Supabase stores metrics and activity data, with Realtime subscriptions pushing live updates to the browser.
The architecture uses Next.js App Router with a Server Component for the initial data load, client components for live-updating metric tiles and Recharts sparklines, a Supabase Realtime subscription for push updates, and an API route for external metric ingestion with API key authentication.
Final result
An internal dashboard with real-time metric tiles, sparkline charts, activity feed, configurable widgets, and an API endpoint for external metric ingestion.
Tech stack
Prerequisites
- A V0 account (Premium recommended for real-time features)
- A Supabase project (free tier works — Realtime is included)
- Basic understanding of KPIs and metrics you want to track
- No advanced coding experience needed
Build steps
Set up the project and metrics schema
Open V0 and create a new project. Connect Supabase via the Connect panel. Create the schema for metrics, activity logs, and dashboard widgets.
1// Paste this prompt into V0's AI chat:2// Build an internal metrics dashboard. Create a Supabase schema with:3// 1. metrics: id (uuid PK), metric_name (text), metric_value (numeric), recorded_at (timestamptz default now()), source (text)4// 2. activity_log: id (uuid PK), user_id (uuid FK to auth.users), action (text), entity_type (text), entity_id (text), metadata (jsonb), created_at (timestamptz default now())5// 3. dashboard_widgets: id (uuid PK), widget_type (text check in 'number','sparkline','bar_chart','activity_feed','table'), title (text), query_config (jsonb), position (int), size (text check in 'small','medium','large')6// Enable Realtime on the metrics table via Supabase Dashboard → Database → Realtime7// Seed dashboard_widgets with 5 default widgets: Total Users (number), Revenue Today (number), Signups Trend (sparkline), Activity Feed (activity_feed), Top Pages (table)8// Seed some sample metrics data for testing9// Add RLS: authenticated users can read all metrics and activity, only service role can insert metricsPro tip: Use Design Mode (Option+D) to visually reposition dashboard cards and adjust spacing without burning credits — perfect for getting the layout right.
Expected result: Database schema created with metrics, activity_log, and dashboard_widgets tables. Realtime enabled on the metrics table for live updates.
Build the dashboard grid with metric tiles
Create the main dashboard page with a responsive grid of metric Cards. Number widgets show the current value, sparkline widgets show trends, and all tiles update in real time.
1// Paste this prompt into V0's AI chat:2// Build the dashboard at app/dashboard/page.tsx.3// Requirements:4// - Server Component that fetches dashboard_widgets and initial metric values5// - Responsive CSS grid: 3 columns desktop, 2 tablet, 1 mobile6// - Widget types:7// 1. 'number' Card: large metric_value, metric_name label, percentage change from previous period8// 2. 'sparkline' Card: small Recharts LineChart showing last 30 data points (wrap in 'use client')9// 3. 'bar_chart' Card: Recharts BarChart for categorical data (wrap in 'use client')10// 4. 'activity_feed': last 10 activity_log entries as a compact list11// 5. 'table': Table widget showing tabular metric data12// - Tabs for time range: Today, This Week, This Month13// - Each metric Card uses Supabase Realtime subscription to update live:14// Create a 'use client' wrapper component that subscribes to metrics table15// .on('postgres_changes', { event: 'INSERT', filter: 'metric_name=eq.{name}' })16// Unsubscribe on cleanup in useEffect return17// - Use Card for all widgets, Badge for entity types in activity feedExpected result: The dashboard shows a responsive grid of metric widgets that update in real time when new data arrives in the metrics table.
Build the Realtime subscription component
Create the client component that wraps metric tiles with Supabase Realtime subscriptions for live updates.
1// Paste this prompt into V0's AI chat:2// Build a RealtimeMetric client component at components/realtime-metric.tsx.3// Requirements:4// - 'use client' component that accepts metricName (string) and initialValue (number)5// - useEffect that subscribes to Supabase Realtime:6// const channel = supabase7// .channel(`metric-${metricName}`)8// .on('postgres_changes', {9// event: 'INSERT',10// schema: 'public',11// table: 'metrics',12// filter: `metric_name=eq.${metricName}`13// }, (payload) => setValue(payload.new.metric_value))14// .subscribe()15// - Cleanup: return () => { supabase.removeChannel(channel) }16// - Animate value changes with a brief pulse animation (scale then back)17// - Show a green dot indicator when connected, red when disconnected18// - Display the value with appropriate formatting (number, currency, percentage)19// - Also build RealtimeSparkline: same pattern but appends new values to a data array for the Recharts LineChart20// - Use Card wrapper with the metric title and a Badge showing 'Live' in greenPro tip: Always unsubscribe from Supabase Realtime channels in the useEffect cleanup function to prevent memory leaks when components unmount.
Expected result: Metric tiles show live-updating values with pulse animations and a green Live badge. Sparklines append new data points in real time.
Build the metric ingestion API
Create an API route that external services call to push metrics into the dashboard. The endpoint uses API key authentication for security.
1// Paste this prompt into V0's AI chat:2// Build a metric ingestion API at app/api/metrics/route.ts.3// Requirements:4// - POST handler that accepts JSON body: { metric_name: string, metric_value: number, source?: string }5// - API key authentication: check x-api-key header against DASHBOARD_API_KEY env var6// - Validate input with Zod: metric_name non-empty, metric_value is number, source optional string7// - Insert into metrics table using Supabase service role client8// - Return 201 with the inserted metric9// - GET handler for batch retrieval: accepts ?metric_name and ?from query params10// - Return 401 for invalid API key, 400 for invalid payload11// - Support batch inserts: accept array of metrics in a single POST12// - Rate limit: simple in-memory counter (100 requests per minute per API key)13// - Also build app/api/activity/route.ts for logging activity events with same auth patternExpected result: External services can POST metrics via API key-authenticated endpoint. Inserted metrics trigger Realtime updates on the dashboard automatically.
Add widget configuration and activity feed
Build the dashboard settings page for configuring widgets and the activity feed component showing recent user actions.
1// Paste this prompt into V0's AI chat:2// Build settings and activity feed:3// 1. app/dashboard/settings/page.tsx — widget configuration:4// - List of current widgets as Card components with title Input, widget_type Select, size Select5// - Reorder widgets by dragging (update position field)6// - Sheet sidebar for advanced query_config editing (which metric_name to display, chart colors)7// - "Add Widget" Button with Dialog for new widget creation8// - "Delete Widget" Button with AlertDialog confirmation9// - Server Actions for widget CRUD and reordering10//11// 2. Activity feed component:12// - Real-time activity feed using Supabase Realtime on activity_log table13// - Each entry: timestamp, user avatar, action description, entity_type Badge, entity_id link14// - New entries slide in from the top with animation15// - Select filter for entity_type (user, order, payment, system)16// - "View All" link to a full activity log page17// - Compact mode (for dashboard widget) and expanded mode (for dedicated page)Expected result: Dashboard layout is configurable via the settings page. The activity feed shows live user actions with type badges and real-time animation.
Complete code
1import { createClient } from '@supabase/supabase-js'2import { NextRequest, NextResponse } from 'next/server'3import { z } from 'zod'45const supabase = createClient(6 process.env.SUPABASE_URL!,7 process.env.SUPABASE_SERVICE_ROLE_KEY!8)910const metricSchema = z.object({11 metric_name: z.string().min(1).max(100),12 metric_value: z.number(),13 source: z.string().max(50).optional(),14})1516const batchSchema = z.union([17 metricSchema,18 z.array(metricSchema).min(1).max(100),19])2021export async function POST(request: NextRequest) {22 const apiKey = request.headers.get('x-api-key')23 if (apiKey !== process.env.DASHBOARD_API_KEY) {24 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })25 }2627 const body = await request.json()28 const parsed = batchSchema.parse(body)29 const metrics = Array.isArray(parsed) ? parsed : [parsed]3031 const { data, error } = await supabase32 .from('metrics')33 .insert(metrics)34 .select()3536 if (error) {37 return NextResponse.json({ error: error.message }, { status: 500 })38 }3940 return NextResponse.json({ inserted: data }, { status: 201 })41}4243export async function GET(request: NextRequest) {44 const apiKey = request.headers.get('x-api-key')45 if (apiKey !== process.env.DASHBOARD_API_KEY) {46 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })47 }4849 const { searchParams } = new URL(request.url)50 const name = searchParams.get('metric_name')51 const from = searchParams.get('from')5253 let query = supabase.from('metrics').select('*').order('recorded_at', { ascending: false }).limit(100)54 if (name) query = query.eq('metric_name', name)55 if (from) query = query.gte('recorded_at', from)5657 const { data, error } = await query58 if (error) return NextResponse.json({ error: error.message }, { status: 500 })5960 return NextResponse.json({ metrics: data })61}Customization ideas
Add alerting rules
Define threshold alerts per metric (e.g., error rate > 5%) that send Slack notifications when triggered.
Add user-specific dashboards
Let each team member create their own dashboard layout with different widget selections and arrangements.
Add data export
Build a CSV/JSON export feature for metric data with date range filtering for offline analysis.
Add embedded analytics
Create embeddable iframe widgets that other internal tools can display using a signed token for authentication.
Common pitfalls
Pitfall: Not unsubscribing from Realtime channels on unmount
How to avoid: Always return a cleanup function from useEffect that calls supabase.removeChannel(channel) when the component unmounts.
Pitfall: Using the anon key for the metric ingestion API
How to avoid: Create a dedicated Supabase client in the API route using SUPABASE_SERVICE_ROLE_KEY (never NEXT_PUBLIC_) for server-side metric insertion.
Pitfall: Querying all metrics without time bounds
How to avoid: Always include a time range filter (recorded_at >= cutoff) in metric queries. Use Tabs to let users select today, this week, or this month.
Pitfall: Not enabling Realtime on the metrics table
How to avoid: Enable Realtime on the metrics table via Supabase Dashboard → Database → Realtime → toggle the table on.
Best practices
- Always unsubscribe from Supabase Realtime channels in the useEffect cleanup to prevent memory leaks.
- Use SUPABASE_SERVICE_ROLE_KEY (never NEXT_PUBLIC_) for the metric ingestion API route that external services call.
- Enable Realtime explicitly on the metrics table in Supabase Dashboard — it is not on by default.
- Use Design Mode (Option+D) to visually adjust dashboard card layout and spacing without burning credits.
- Add time range filters to all metric queries to prevent unbounded data loading as the table grows.
- Use connection pooling via Supavisor for serverless API route connections to prevent connection exhaustion.
- Validate all ingested metrics with Zod in the API route to reject malformed data before insertion.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a real-time dashboard with Next.js and Supabase. I need a 'use client' component that subscribes to Supabase Realtime postgres_changes on a metrics table, updates the displayed value on INSERT events, and properly cleans up the subscription on unmount. Show me the component code, the Supabase channel setup, and how to animate value changes.
Build the RealtimeMetric component for an internal dashboard. Create a 'use client' component that accepts metricName and initialValue props. In useEffect, subscribe to Supabase Realtime on the metrics table filtered by metric_name. On INSERT events, update the displayed value with a pulse animation. Show a green Live Badge when connected. Return the cleanup function to removeChannel. Format values as numbers, currency, or percentages based on a format prop.
Frequently asked questions
Can I build this with the free V0 plan?
The core metric tiles and activity feed fit within the free tier. V0 Premium is recommended for the full system with Realtime components and widget configuration.
How do real-time updates work?
Supabase Realtime sends postgres_changes events via WebSocket when rows are inserted into the metrics table. A 'use client' component subscribes to these events and updates the displayed value instantly.
How do I send metrics from external services?
POST JSON to /api/metrics with an x-api-key header. The endpoint validates the payload with Zod and inserts into the metrics table, which triggers Realtime updates on the dashboard.
Can I customize the dashboard layout?
Yes. The dashboard_widgets table stores each widget's type, title, position, and size. The settings page lets you add, remove, reorder, and configure widgets.
What about Supabase Realtime limits?
The free tier supports up to 200 concurrent connections and 2 million Realtime messages per month. For most internal dashboards with small teams, this is more than sufficient.
How do I deploy?
Publish via V0's Share menu. Set SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in the Vars tab (no NEXT_PUBLIC_ prefix for the ingestion route). Generate a DASHBOARD_API_KEY and distribute to services that push metrics.
Can RapidDev help build a custom dashboard platform?
Yes. RapidDev has built 600+ apps including real-time analytics platforms with custom visualizations, alerting, and embedded reporting. Book a free consultation to discuss your needs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation