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
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
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.
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.
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.
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 controls5// - Input for report name6// - 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 range11// - 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 Recharts13// - Updates as config changes14// - Renders the selected chart type with real data from Supabase15// - Top bar: Save Button (Server Action), Export DropdownMenu (CSV/PDF/PNG)16// - Use shadcn/ui Card, Select, Input, Calendar, Popover, Tabs, DropdownMenu17// - '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.
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.
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const { report_id } = await req.json()1112 const { data: report } = await supabase13 .from('reports')14 .select('query_config, chart_type')15 .eq('id', report_id)16 .single()1718 if (!report) {19 return NextResponse.json({ error: 'Report not found' }, { status: 404 })20 }2122 const config = report.query_config as {23 table: string24 dimension: string25 metric: string26 metric_type: string27 date_field?: string28 start_date?: string29 end_date?: string30 filters?: Array<{ field: string; operator: string; value: string }>31 }3233 let query = supabase.from(config.table).select('*')3435 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 }4142 config.filters?.forEach((f) => {43 switch (f.operator) {44 case 'equals': query = query.eq(f.field, f.value); break45 case 'greater': query = query.gt(f.field, f.value); break46 case 'less': query = query.lt(f.field, f.value); break47 case 'contains': query = query.ilike(f.field, `%${f.value}%`); break48 }49 })5051 const { data, error } = await query5253 if (error) {54 return NextResponse.json({ error: error.message }, { status: 500 })55 }5657 // Aggregate client-side for flexibility58 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) ?? 063 grouped.set(key, config.metric_type === 'count' ? current + 1 : current + val)64 })6566 const chartData = Array.from(grouped.entries()).map(([name, value]) => ({67 name,68 value,69 }))7071 // Save snapshot72 await supabase.from('report_snapshots').insert({73 report_id,74 data: chartData,75 })7677 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.
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.
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 header6// - For PDF: use @react-pdf/renderer to create a PDF document with:7// - Report title and date range header8// - Table of the data with styled headers and alternating row colors9// - Summary statistics (total, average)10// - Footer with generation timestamp11// - Return as downloadable PDF with Content-Disposition header12// - Set maxDuration = 30 in the route config for large reports13// - Store the generated file URL in report_snapshots.file_url14// Also add export buttons to the report viewer:15// - DropdownMenu with CSV, PDF, PNG options16// - 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.
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.
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 finding5// - Grid of report Cards showing: name, chart type icon, last generated timestamp, shared count Badge6// - Each Card links to the report viewer7// - "New Report" Button to create blank report → redirect to builder8// 2. Report viewer at app/reports/[id]/page.tsx:9// - Full-width Recharts chart rendering the latest snapshot data10// - Tabs to switch between Chart view and Table view11// - DatePicker to re-run the report for a different date range12// - Export DropdownMenu (CSV/PDF)13// - Share Button opens Dialog with:14// - Input for team member email15// - Select for permission (view/edit)16// - Table of existing shares with role Badge and remove Button17// - Server Action shareReport()18// 3. Server Components for data fetching, 'use client' for interactive chart and sharing dialogExpected 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
1import { NextRequest, NextResponse } from 'next/server'2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.SUPABASE_URL!,6 process.env.SUPABASE_SERVICE_ROLE_KEY!7)89export async function POST(req: NextRequest) {10 const { report_id } = await req.json()1112 const { data: report } = await supabase13 .from('reports')14 .select('query_config, chart_type')15 .eq('id', report_id)16 .single()1718 if (!report) {19 return NextResponse.json({ error: 'Not found' }, { status: 404 })20 }2122 const config = report.query_config as {23 table: string24 dimension: string25 metric: string26 metric_type: string27 date_field?: string28 start_date?: string29 end_date?: string30 }3132 let query = supabase.from(config.table).select('*')3334 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 }4041 const { data, error } = await query42 if (error) {43 return NextResponse.json({ error: error.message }, { status: 500 })44 }4546 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 })5354 const chartData = [...grouped.entries()].map(([name, value]) => ({55 name,56 value,57 }))5859 await supabase.from('report_snapshots').insert({60 report_id,61 data: chartData,62 })6364 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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation