Skip to main content
RapidDev - Software Development Agency

How to Build Reporting tool with V0

Build a customizable business reporting tool with V0 using Next.js, Supabase, and Recharts that lets users create reports with chart visualizations, configure filters, and export to CSV or PDF. Features a drag-and-drop report builder, scheduled generation, and team sharing — all in about 1-2 hours.

What you'll build

  • Report builder with configurable data queries, filters, and chart type selection
  • Interactive chart visualizations using Recharts (bar, line, pie, area charts)
  • CSV and PDF export functionality for sharing reports outside the app
  • Report sharing with view/edit permissions for team members
  • Saved report library with last-generated timestamps and quick search
  • Date range filters using shadcn/ui Calendar and Popover (DatePicker pattern)
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate11 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a customizable business reporting tool with V0 using Next.js, Supabase, and Recharts that lets users create reports with chart visualizations, configure filters, and export to CSV or PDF. Features a drag-and-drop report builder, scheduled generation, and team sharing — all in about 1-2 hours.

What you're building

Every business needs reports — sales summaries, user growth charts, financial breakdowns. Building reports in spreadsheets is tedious and error-prone. A dedicated reporting tool lets your team create, save, and schedule reports with consistent visualizations and easy sharing.

V0 generates the report builder interface, chart components, and export logic from prompts. Recharts is bundled with every V0 project for professional data visualization. Supabase stores both the report configurations and the source data.

The architecture uses Next.js App Router with Server Components for the report list and viewer, API routes for report generation and export, Recharts for all chart rendering, Server Actions for saving and sharing reports, and @react-pdf/renderer for PDF export in API routes.

Final result

A business reporting tool with a visual report builder, interactive Recharts charts, date range and dimension filters, CSV and PDF export, report sharing with permissions, and a saved report library.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
RechartsData Visualization
@react-pdf/rendererPDF Generation

Prerequisites

  • A V0 account (Premium recommended for the report builder complexity)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • Sample business data in Supabase tables (sales, users, orders, etc.)
  • Basic understanding of what reports your team needs

Build steps

1

Set up the project and reporting schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Create the schema for reports, snapshots, data sources, and sharing. This project uses Supabase as both the report metadata store and the data source.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a reporting tool. Create a Supabase schema with:
3// 1. reports: id (uuid PK), owner_id (uuid FK), name (text), description (text), query_config (jsonb), chart_type (text check bar/line/pie/table/area), filters (jsonb), is_scheduled (boolean default false), schedule_cron (text nullable), created_at (timestamptz), updated_at (timestamptz)
4// 2. report_snapshots: id (uuid PK), report_id (uuid FK), data (jsonb), generated_at (timestamptz), file_url (text nullable)
5// 3. data_sources: id (uuid PK), owner_id (uuid FK), name (text), type (text check supabase/api/csv), connection_config (jsonb), created_at (timestamptz)
6// 4. shared_reports: id (uuid PK), report_id (uuid FK), shared_with (uuid FK), permission (text check view/edit), created_at (timestamptz)
7// RLS: owners can CRUD their own reports. Shared users can read or edit based on permission.
8// Generate SQL migration and TypeScript types.

Pro tip: Use V0's Connect panel to provision Supabase where both the report metadata AND the source data live — one connection handles everything.

Expected result: Supabase is connected with reports, snapshots, data sources, and sharing tables. RLS policies enforce owner-based and permission-based access control.

2

Build the report builder interface

Create the report editor page where users configure their report — selecting data source, dimensions, metrics, filters, chart type, and date range. The builder constructs a query_config object stored in the reports table.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a report builder at app/reports/[id]/edit/page.tsx.
3// Requirements:
4// - Left panel: configuration controls
5// - Input for report name
6// - Select for chart type (bar/line/pie/table/area)
7// - Select for data source table (populated from Supabase information_schema)
8// - Select for dimension field (group by)
9// - Select for metric field (count/sum/avg)
10// - DatePicker (Calendar + Popover) for start and end date range
11// - Additional filter rows: Select for field, Select for operator (equals/greater/less/contains), Input for value. Add/Remove Buttons.
12// - Right panel: live chart preview using Recharts
13// - Updates as config changes
14// - Renders the selected chart type with real data from Supabase
15// - Top bar: Save Button (Server Action), Export DropdownMenu (CSV/PDF/PNG)
16// - Use shadcn/ui Card, Select, Input, Calendar, Popover, Tabs, DropdownMenu
17// - 'use client' for the interactive builder, Server Action saveReport()

Expected result: A two-panel report builder with configuration controls on the left and live Recharts preview on the right. Changing filters or chart type immediately updates the visualization.

3

Create the report generation API route

Build the API route that executes a report's query configuration against the data source and returns aggregated data. This constructs Supabase queries programmatically from the stored config — never executing raw SQL from user input.

app/api/reports/generate/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 POST(req: NextRequest) {
10 const { report_id } = await req.json()
11
12 const { data: report } = await supabase
13 .from('reports')
14 .select('query_config, chart_type')
15 .eq('id', report_id)
16 .single()
17
18 if (!report) {
19 return NextResponse.json({ error: 'Report not found' }, { status: 404 })
20 }
21
22 const config = report.query_config as {
23 table: string
24 dimension: string
25 metric: string
26 metric_type: string
27 date_field?: string
28 start_date?: string
29 end_date?: string
30 filters?: Array<{ field: string; operator: string; value: string }>
31 }
32
33 let query = supabase.from(config.table).select('*')
34
35 if (config.start_date && config.date_field) {
36 query = query.gte(config.date_field, config.start_date)
37 }
38 if (config.end_date && config.date_field) {
39 query = query.lte(config.date_field, config.end_date)
40 }
41
42 config.filters?.forEach((f) => {
43 switch (f.operator) {
44 case 'equals': query = query.eq(f.field, f.value); break
45 case 'greater': query = query.gt(f.field, f.value); break
46 case 'less': query = query.lt(f.field, f.value); break
47 case 'contains': query = query.ilike(f.field, `%${f.value}%`); break
48 }
49 })
50
51 const { data, error } = await query
52
53 if (error) {
54 return NextResponse.json({ error: error.message }, { status: 500 })
55 }
56
57 // Aggregate client-side for flexibility
58 const grouped = new Map<string, number>()
59 data?.forEach((row: Record<string, unknown>) => {
60 const key = String(row[config.dimension] ?? 'Unknown')
61 const val = Number(row[config.metric] ?? 0)
62 const current = grouped.get(key) ?? 0
63 grouped.set(key, config.metric_type === 'count' ? current + 1 : current + val)
64 })
65
66 const chartData = Array.from(grouped.entries()).map(([name, value]) => ({
67 name,
68 value,
69 }))
70
71 // Save snapshot
72 await supabase.from('report_snapshots').insert({
73 report_id,
74 data: chartData,
75 })
76
77 return NextResponse.json({ data: chartData, chart_type: report.chart_type })
78}

Expected result: POST /api/reports/generate executes the report's query config against Supabase, aggregates data by dimension and metric, saves a snapshot, and returns chart-ready data.

4

Add CSV and PDF export functionality

Create an export API route that generates CSV or PDF files from report data. CSV is generated server-side as text, PDF uses @react-pdf/renderer for formatted output.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a report export API at app/api/reports/export/route.ts.
3// Requirements:
4// - Accepts POST with { report_id, format: 'csv' | 'pdf' }
5// - For CSV: generate comma-separated text from the latest report_snapshot data, return as downloadable file with Content-Disposition header
6// - For PDF: use @react-pdf/renderer to create a PDF document with:
7// - Report title and date range header
8// - Table of the data with styled headers and alternating row colors
9// - Summary statistics (total, average)
10// - Footer with generation timestamp
11// - Return as downloadable PDF with Content-Disposition header
12// - Set maxDuration = 30 in the route config for large reports
13// - Store the generated file URL in report_snapshots.file_url
14// Also add export buttons to the report viewer:
15// - DropdownMenu with CSV, PDF, PNG options
16// - PNG export uses canvas-to-image on the Recharts chart (client-side)

Pro tip: Set export const maxDuration = 30 in the API route for Vercel serverless — PDF generation can be memory-intensive for large reports.

Expected result: Reports can be exported as CSV or PDF files. CSV downloads instantly as formatted text. PDF generates a styled document with tables and summary statistics.

5

Build the report library and sharing

Create the main reports page showing all saved reports with quick search, and add sharing functionality so team members can view or edit reports.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a report library and sharing:
3// 1. Report list at app/reports/page.tsx:
4// - Command search bar for quick report finding
5// - Grid of report Cards showing: name, chart type icon, last generated timestamp, shared count Badge
6// - Each Card links to the report viewer
7// - "New Report" Button to create blank report → redirect to builder
8// 2. Report viewer at app/reports/[id]/page.tsx:
9// - Full-width Recharts chart rendering the latest snapshot data
10// - Tabs to switch between Chart view and Table view
11// - DatePicker to re-run the report for a different date range
12// - Export DropdownMenu (CSV/PDF)
13// - Share Button opens Dialog with:
14// - Input for team member email
15// - Select for permission (view/edit)
16// - Table of existing shares with role Badge and remove Button
17// - Server Action shareReport()
18// 3. Server Components for data fetching, 'use client' for interactive chart and sharing dialog

Expected result: A report library with searchable Cards, a report viewer with interactive charts and export options, and a sharing Dialog for team collaboration.

Complete code

app/api/reports/generate/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 POST(req: NextRequest) {
10 const { report_id } = await req.json()
11
12 const { data: report } = await supabase
13 .from('reports')
14 .select('query_config, chart_type')
15 .eq('id', report_id)
16 .single()
17
18 if (!report) {
19 return NextResponse.json({ error: 'Not found' }, { status: 404 })
20 }
21
22 const config = report.query_config as {
23 table: string
24 dimension: string
25 metric: string
26 metric_type: string
27 date_field?: string
28 start_date?: string
29 end_date?: string
30 }
31
32 let query = supabase.from(config.table).select('*')
33
34 if (config.start_date && config.date_field) {
35 query = query.gte(config.date_field, config.start_date)
36 }
37 if (config.end_date && config.date_field) {
38 query = query.lte(config.date_field, config.end_date)
39 }
40
41 const { data, error } = await query
42 if (error) {
43 return NextResponse.json({ error: error.message }, { status: 500 })
44 }
45
46 const grouped = new Map<string, number>()
47 data?.forEach((row: Record<string, unknown>) => {
48 const key = String(row[config.dimension] ?? 'Other')
49 const val = Number(row[config.metric] ?? 0)
50 grouped.set(key, (grouped.get(key) ?? 0) +
51 (config.metric_type === 'count' ? 1 : val))
52 })
53
54 const chartData = [...grouped.entries()].map(([name, value]) => ({
55 name,
56 value,
57 }))
58
59 await supabase.from('report_snapshots').insert({
60 report_id,
61 data: chartData,
62 })
63
64 return NextResponse.json({ data: chartData })
65}

Customization ideas

Add scheduled report generation

Use Vercel Cron Jobs to automatically generate reports on a schedule (daily, weekly, monthly) and email the results to stakeholders via Resend.

Build a dashboard with multiple widgets

Create a dashboard view where users can pin multiple reports as widgets in a grid layout, creating a customizable business intelligence overview.

Add drill-down functionality

Let users click on chart segments to drill down into the underlying data, adding the clicked dimension as a filter and regenerating the report.

Connect external data sources

Add API-based data sources so users can pull data from Google Analytics, Stripe, or other services alongside their Supabase data.

Common pitfalls

Pitfall: Executing raw SQL from user-configured query_config

How to avoid: Never execute raw SQL from user input. Construct Supabase queries programmatically using the query builder pattern (.from().select().gte().lte().eq()) based on the config values.

Pitfall: Not setting maxDuration for PDF export API routes

How to avoid: Add export const maxDuration = 30 to the API route file. This gives Vercel serverless functions 30 seconds to complete the PDF generation.

Pitfall: Loading all report data client-side for chart rendering

How to avoid: Aggregate data server-side in the API route and return only the chart-ready grouped data. The client receives dozens of data points, not thousands of rows.

Best practices

  • Never execute raw SQL from user input — use the Supabase query builder pattern for programmatic query construction
  • Aggregate data server-side and return only chart-ready data to the client to minimize bandwidth
  • Use Recharts (bundled with V0 projects) for all chart rendering — no additional packages needed
  • Set maxDuration = 30 in export API routes for large PDF/CSV generation
  • Use V0's Connect panel for Supabase provisioning so report data and metadata share one connection
  • Save report snapshots for historical comparison without re-querying the source data

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a reporting tool with Next.js App Router and Supabase. I need a dynamic query builder that takes a report configuration (table name, dimension field, metric field, metric type, date range, filters) and constructs a Supabase query programmatically. It should never use raw SQL. Write the server-side function and the aggregation logic that returns chart-ready data grouped by dimension.

Build Prompt

Create a report chart component that renders different Recharts chart types based on a chart_type prop. Support bar (BarChart), line (LineChart), pie (PieChart), area (AreaChart), and table (shadcn/ui Table). Accept data as an array of {name, value} objects. Include proper axes, tooltips, legends, and responsive containers for each type. Mark as 'use client'.

Frequently asked questions

What V0 plan do I need for a reporting tool?

V0 Free works for the basic build, but Premium ($20/month) is recommended because the report builder has complex interactive pages that benefit from prompt queuing.

Can I connect to data sources other than Supabase?

The base build uses Supabase as the data source. You can extend it by adding API-based data sources that fetch from external services (Google Analytics, Stripe) and normalize the data into the same chart-ready format.

How does PDF export work in a serverless environment?

@react-pdf/renderer runs entirely server-side in the API route — no browser APIs needed. Set maxDuration = 30 in the route config for larger reports. The generated PDF is returned as a downloadable response.

Can team members collaborate on reports?

Yes. The sharing system lets you invite team members by email with view or edit permissions. Shared reports appear in their report library alongside their own reports.

How do I deploy the reporting tool?

Click Share then Publish to Production in V0. Set SUPABASE_SERVICE_ROLE_KEY in the Vars tab without NEXT_PUBLIC_ prefix. Supabase connection is auto-configured from the Connect panel.

Can RapidDev help build a custom reporting platform?

Yes. RapidDev has built 600+ apps including custom BI platforms with scheduled reports, white-label dashboards, and multi-source data integration. Book a free consultation to discuss your 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.