Build an internal admin dashboard in Lovable with Supabase Postgres views, KPI metric cards, interactive charts, a drill-down DataTable, and a real-time metrics ticker. The result is a live business intelligence panel your whole team can use — built without touching a server.
What you're building
An internal dashboard in Lovable surfaces operational metrics from your existing Supabase tables without duplicating data. Instead of querying raw tables in the UI, you create Postgres views that aggregate the numbers — total revenue this month, active users, open tickets — so each KPI card fires a single fast SELECT against a view rather than a complex JOIN.
The real-time metrics ticker subscribes to a Supabase Realtime channel. When a new row lands in a key table (an order, a signup, a resolved ticket), a database trigger inserts a lightweight event row. The ticker's subscription picks this up and animates a new entry into the scrolling feed at the top of the page.
The DataTable uses TanStack Table v8 for client-side sorting and pagination. Clicking any row opens a Sheet panel that fetches the full detail record from Supabase — keeping the main table query lightweight while still giving users the ability to drill into any row.
Final result
A real-time admin dashboard with live KPI cards, trend charts, a drillable DataTable, and a scrolling event ticker — all protected behind role-based auth.
Tech stack
Prerequisites
- Lovable Pro account
- Supabase project with at least one existing table containing business data
- VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in Cloud tab → Secrets
- A profiles table with a role column (admin/member) linked to auth.users
- Familiarity with Lovable's Cloud tab and the Publish flow
Build steps
Create Postgres views for KPI aggregation
Ask Lovable to generate SQL views in Supabase that pre-aggregate your key metrics. Views run on the database and return a single row of numbers — much faster than running aggregation queries in React.
1Create Supabase Postgres views for a business dashboard. Assume these source tables exist (or create mock versions):2- orders: id, user_id, total, status, created_at3- users: id, email, created_at4- support_tickets: id, user_id, status, resolved_at, created_at56Create these views:71. kpi_summary — columns: total_revenue (sum of orders.total where status='paid'), revenue_last_period, active_users (count distinct users last 30 days), new_users_this_period, open_tickets (count where status='open'), tickets_last_period. Include period-over-period delta as a percentage.82. daily_revenue — columns: day (date), revenue (sum), order_count for the last 90 days93. user_signups_daily — columns: day, signups for the last 90 days1011Grant SELECT on these views to the authenticated role so RLS-authenticated users can read them.Pro tip: Add a SECURITY DEFINER clause to the views so they bypass RLS and aggregate across all rows — then protect access at the route level with role checking rather than per-row policies.
Expected result: Lovable generates the SQL and runs it via Supabase migrations. You can query SELECT * FROM kpi_summary in the Supabase SQL editor and see one row of aggregated numbers.
Build KPI cards with delta badges
Create a row of KPI cards at the top of the dashboard. Each card shows the current metric value, a trend percentage compared to last period, and an up/down arrow badge color-coded green or red.
1import { useQuery } from '@tanstack/react-query'2import { supabase } from '@/integrations/supabase/client'3import { Card, CardContent } from '@/components/ui/card'4import { Badge } from '@/components/ui/badge'5import { TrendingUp, TrendingDown } from 'lucide-react'67type KpiSummary = {8 total_revenue: number9 revenue_last_period: number10 active_users: number11 new_users_this_period: number12 open_tickets: number13 tickets_last_period: number14}1516function delta(current: number, previous: number) {17 if (!previous) return 018 return Math.round(((current - previous) / previous) * 100)19}2021export function KpiCards() {22 const { data } = useQuery<KpiSummary>({23 queryKey: ['kpi_summary'],24 queryFn: async () => {25 const { data, error } = await supabase.from('kpi_summary').select('*').single()26 if (error) throw error27 return data28 },29 staleTime: 60_000,30 })3132 const cards = [33 { label: 'Revenue', value: data ? `$${data.total_revenue.toLocaleString()}` : '—', pct: data ? delta(data.total_revenue, data.revenue_last_period) : 0 },34 { label: 'Active Users', value: data?.active_users.toLocaleString() ?? '—', pct: 0 },35 { label: 'New Users', value: data?.new_users_this_period.toLocaleString() ?? '—', pct: 0 },36 { label: 'Open Tickets', value: data?.open_tickets.toLocaleString() ?? '—', pct: data ? delta(data.open_tickets, data.tickets_last_period) : 0 },37 ]3839 return (40 <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">41 {cards.map((c) => (42 <Card key={c.label}>43 <CardContent className="pt-6">44 <p className="text-sm text-muted-foreground">{c.label}</p>45 <p className="mt-1 text-2xl font-bold">{c.value}</p>46 {c.pct !== 0 && (47 <Badge variant={c.pct > 0 ? 'default' : 'destructive'} className="mt-2 gap-1">48 {c.pct > 0 ? <TrendingUp className="h-3 w-3" /> : <TrendingDown className="h-3 w-3" />}49 {Math.abs(c.pct)}%50 </Badge>51 )}52 </CardContent>53 </Card>54 ))}55 </div>56 )57}Pro tip: Wrap the KpiCards query with a refetchInterval of 5 * 60 * 1000 so the numbers stay fresh during long dashboard sessions without requiring a manual page refresh.
Expected result: Four metric cards appear at the top of the dashboard showing current values. Cards with positive deltas show a green upward badge; negative deltas show red downward badges.
Add trend charts from the daily views
Render line charts for daily revenue and user signups using the Postgres views created in step 1. Each chart reads from its view and renders with Recharts inside a shadcn/ui Card.
1Build a TrendsSection component at src/components/dashboard/TrendsSection.tsx.23Requirements:4- Fetch data from daily_revenue view (day, revenue, order_count)5- Fetch data from user_signups_daily view (day, signups)6- Render two side-by-side Cards on md+ screens, stacked on mobile7- Left card: AreaChart for revenue with a gradient fill in the primary color8- Right card: BarChart for daily signups9- Both charts: XAxis with date labels formatted as 'MMM d' using date-fns format(), YAxis, Tooltip, ResponsiveContainer height={240}10- Show Skeleton placeholders while loading11- Add a small Select in each card header to switch between 30d / 60d / 90d windows — update the Supabase query with a .gte('day', cutoffDate) filterExpected result: Two charts appear below the KPI cards. The revenue chart shows an area gradient; the signups chart shows daily bars. The period selector re-fetches data for the chosen window.
Build the DataTable with drill-down Sheet
Add a DataTable for the most recent orders. Clicking any row fetches the full order record and opens a Sheet panel with complete order details, line items, and customer info.
1Build an OrdersTable component at src/components/dashboard/OrdersTable.tsx.23Requirements:4- Fetch latest 100 orders from Supabase: select id, user_id, total, status, created_at. Join with users to get email.5- Use TanStack Table v8 with these columns: Order ID (truncated), Customer Email, Total (formatted as currency), Status (shadcn/ui Badge: paid=green, pending=yellow, failed=red), Date6- Enable client-side sorting on all columns7- Add a search input above the table filtering by email or order ID8- On row click, fetch the full order from Supabase including a hypothetical order_items sub-table, then open a shadcn/ui Sheet9- Sheet content: customer name and email, order status Badge, total, line items list (product name, qty, unit price), created_at10- Add a CSV export button that downloads the visible rows as a CSV file using a Blob URLPro tip: Implement server-side pagination by fetching rows in pages of 25 using Supabase's .range(from, to) method — this keeps the initial page load fast even when there are thousands of orders.
Expected result: A sortable, searchable orders table appears on the dashboard. Clicking any row slides open the Sheet showing the full order details. The CSV export button downloads all visible rows.
Add the real-time metrics ticker
Build a scrolling ticker at the top of the dashboard that shows live events — new orders, signups, resolved tickets — using Supabase Realtime subscriptions.
1Build a MetricsTicker component at src/components/dashboard/MetricsTicker.tsx.23Requirements:4- Create a dashboard_events table in Supabase: id, event_type (new_order|new_user|ticket_resolved), message (text), metadata (jsonb), created_at. Enable Realtime on it.5- Subscribe to INSERT events on dashboard_events using supabase.channel('dashboard_events').on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'dashboard_events' }, handler).subscribe()6- Maintain a local state array of the last 20 events (prepend new events, slice to 20)7- Render a horizontal scrolling ticker bar at the top of the dashboard8- Each ticker item shows: a colored dot by event_type (new_order=green, new_user=blue, ticket_resolved=purple), the message text, and a relative time9- Animate new items sliding in from the right using a CSS transition10- Unsubscribe on component unmountExpected result: A scrolling ticker bar appears at the top of the dashboard. When you insert a row into dashboard_events in Supabase's Table Editor, the new event appears in the ticker within one second.
Add role-based route protection
Protect the dashboard route so only users with the admin role in the profiles table can access it. Non-admin users are redirected to a permission denied page.
1Add role-based route protection to the dashboard.23Requirements:4- Create a useAdminGuard hook at src/hooks/useAdminGuard.ts that:5 1. Gets the current session from supabase.auth.getSession()6 2. Fetches the profile row: supabase.from('profiles').select('role').eq('id', userId).single()7 3. Returns { isAdmin: boolean, isLoading: boolean }8- Wrap the Dashboard page component: if isLoading show a full-page Skeleton; if !isAdmin redirect to '/unauthorized'9- Create a simple /unauthorized page with a Card explaining the user doesn't have admin access and a Button to go back to the home page10- Ensure the profiles table has RLS: users can only SELECT their own rowPro tip: Store the role in the user's JWT claims using a Supabase Auth hook so you can read it from the session without an extra database round-trip on every route load.
Expected result: Logging in as a non-admin user and navigating to /dashboard redirects immediately to /unauthorized. Admin users see the full dashboard.
Complete code
1import { useEffect, useState } from 'react'2import { useNavigate } from 'react-router-dom'3import { supabase } from '@/integrations/supabase/client'45export function useAdminGuard() {6 const [isAdmin, setIsAdmin] = useState(false)7 const [isLoading, setIsLoading] = useState(true)8 const navigate = useNavigate()910 useEffect(() => {11 let mounted = true1213 async function check() {14 const { data: { session } } = await supabase.auth.getSession()15 if (!session) {16 navigate('/login')17 return18 }1920 const { data: profile, error } = await supabase21 .from('profiles')22 .select('role')23 .eq('id', session.user.id)24 .single()2526 if (!mounted) return2728 if (error || !profile || profile.role !== 'admin') {29 setIsAdmin(false)30 setIsLoading(false)31 navigate('/unauthorized')32 return33 }3435 setIsAdmin(true)36 setIsLoading(false)37 }3839 check()40 return () => { mounted = false }41 }, [navigate])4243 return { isAdmin, isLoading }44}Customization ideas
Pinnable widgets
Let users pin their most-used KPI cards and charts to a personalized view. Store the widget order in a user_dashboard_config JSONB column in Supabase.
Email digest of daily metrics
Create a Supabase Edge Function triggered by pg_cron at 8am each day that reads the kpi_summary view and sends an HTML email digest to admin users via Resend.
Threshold-based alerts
Add an alerts table where admins configure metric thresholds. A Postgres trigger fires when an order total exceeds the threshold and inserts a row into dashboard_events, triggering the real-time ticker.
Dark mode support
Add a theme toggle in the top navigation. Recharts charts need their stroke and fill colors updated — store the active theme in context and pass chart colors as computed values.
Comparison mode
Add a toggle that overlays two time periods on the trend charts — for example this month vs last month — using Recharts' ability to render multiple Line or Bar components on the same axis.
Common pitfalls
Pitfall: Querying raw tables for KPI numbers instead of using views
How to avoid: Create Postgres views or functions that return pre-aggregated numbers so each KPI card fires one fast SELECT.
Pitfall: Subscribing to Realtime without unsubscribing on unmount
How to avoid: Always return a cleanup function from useEffect that calls supabase.removeChannel(channel).
Pitfall: Fetching all orders into a DataTable without pagination
How to avoid: Use Supabase's .range(0, 24) pagination and implement server-side paging in TanStack Table.
Pitfall: Storing the admin role check only on the client
How to avoid: Enforce access at the Supabase RLS level by adding policies that check for the admin role, not just at the React router level.
Best practices
- Use Postgres views for metric aggregation — they keep query logic in the database where it belongs and simplify the React code.
- Enable Realtime only on tables that need it — each active subscription counts against Supabase's concurrent connection limit.
- Use staleTime in React Query to avoid re-fetching KPI cards on every focus event — KPI data rarely needs sub-minute freshness.
- Add database indexes on the columns used in your view WHERE clauses — created_at, status, and user_id are common candidates.
- Test RLS policies with two different user roles before deploying — admin and regular user — to catch policy gaps early.
- Use shadcn/ui Skeleton components for all loading states to prevent layout shift as data loads.
- Scope the Realtime subscription to the minimum table and event type needed — use filter: 'event_type=eq.new_order' to reduce noise.
- Log all admin actions (exports, role changes, data deletions) to an audit_log table for compliance and debugging.
AI prompts to try
Copy these prompts to build this project faster.
I have a Supabase database with orders, users, and support_tickets tables. Help me write SQL for three Postgres views: one that returns a single row of KPI aggregates (total revenue, active users, open tickets) with period-over-period percentage deltas, one that returns daily revenue for the last 90 days, and one that returns daily user signups for the last 90 days.
Add a widget customization mode to the dashboard. When the user clicks an 'Edit Layout' button, each widget card should show a drag handle and an X button. The user can reorder cards by dragging and remove ones they don't need. Save the layout to a user_dashboard_config table in Supabase. Restore the saved layout on next login.
In Lovable, add a notifications panel to the dashboard using shadcn/ui Sheet. It should open from a bell icon in the top navigation. Inside, show the last 50 rows from the dashboard_events table sorted by created_at desc. Each event has a colored dot by type, a message, and a time. Mark all as read by updating a read_at column when the Sheet opens.
Frequently asked questions
Do I need to create new tables or can the dashboard use my existing ones?
You can use your existing tables. Create Postgres views on top of them that aggregate the metrics you want. The dashboard queries the views rather than the raw tables, so your data structure stays unchanged.
How do I make the real-time ticker work?
You need to enable Realtime on the dashboard_events table in Supabase (Database → Replication → toggle the table). Then, whenever a key event happens (new order, new signup), insert a row into dashboard_events. The ticker's Supabase Realtime subscription picks it up instantly.
Can I show data from multiple Supabase tables in one KPI card?
Yes. Create a Postgres view that JOINs or aggregates across multiple tables and returns a single-row result. The KPI card queries the view with a .single() call and reads each column as a separate metric.
How do I restrict the dashboard to admins only?
Add a role column to your profiles table, set it to 'admin' for the appropriate users, and use the useAdminGuard hook shown in step 6. Also add RLS policies to your metric views so they only return data to admin-role users for defense in depth.
Why are my KPI numbers updating slowly?
If your source tables are large, the Postgres views may be running expensive aggregations on every query. Add indexes on the columns used in WHERE clauses (status, created_at) and consider using materialized views refreshed periodically for the heaviest aggregations.
Can I embed this dashboard inside another application?
Lovable apps are standalone React SPAs. You can iframe the published URL, but the embedded page still requires the user to be authenticated. For truly embeddable widgets, consider building a separate public endpoint that returns pre-computed JSON and renders charts without auth.
What happens to the real-time ticker when the user loses network connection?
The Supabase Realtime client will attempt to reconnect automatically. Add an onError handler to the channel subscription to show a warning badge on the ticker when the connection is lost, and hide it when the connection is restored.
Is RapidDev able to help customize this dashboard for my specific business metrics?
Yes. RapidDev can help you map your existing data model to the views and chart structure, configure RLS for your team, and add custom metrics specific to your industry.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation