Prompt of the Day: Build a Real-Time Dashboard with Supabase
Series: Prompt of the Day — Part 6 of 30 | Track: Prompts | By Tom Hundley
The Saturday Night Meltdown
I got a call on a Saturday night from a developer I'd been mentoring. She'd built a restaurant ordering dashboard for a local chain—tablets in the kitchen, a manager's screen showing live order counts, revenue ticking up in real time. It was beautiful. Customers loved it. Then opening weekend hit.
By 7 PM, the manager's screen had frozen. The kitchen tablets were showing stale data. They were turning tables manually with pen and paper. The culprit? A Supabase Realtime subscription that was never unsubscribed when the manager navigated between views. Every route change created a new WebSocket connection on top of the old one. By Saturday dinner rush, there were dozens of zombie subscriptions hammering the server, the app slowed to a crawl, and Supabase's free-tier connection limit killed the rest.
This is not an edge case. In a June 2025 Reddit thread on r/Supabase, a developer building a restaurant ordering system described the exact same spiral: self-hosted Supabase on Coolify, connections dropping unexpectedly, messages silently failing, the whole thing unreliable enough that the community's top advice was "polling every five seconds would have given you 80% of real-time with none of the complexity."
That's the honest state of the ecosystem. Supabase Realtime is powerful. It's also surprisingly easy to break in production. Today's prompt gets it right from the start.
The Prompt
You are a senior full-stack engineer. Build a real-time analytics dashboard component
in Next.js 14+ (App Router) using Supabase Realtime and TypeScript.
The dashboard should display:
- Live order count (updated on INSERT)
- Running revenue total (updated on INSERT/UPDATE)
- A rolling list of the last 10 orders, newest first
Requirements:
1. Use a Client Component ('use client') — not a Server Component
2. Seed initial data with a server-side fetch before the component mounts
3. Subscribe to Postgres changes on the 'orders' table using supabase.channel()
4. Handle INSERT, UPDATE, and DELETE events explicitly
5. Clean up the subscription in the useEffect return function using supabase.removeChannel(channel)
6. Display a connection status indicator ('Live' / 'Reconnecting...')
7. Track subscription state with useRef to prevent duplicate subscriptions in React StrictMode
8. Add RLS policy SQL comments explaining what access the policy grants
9. Enable realtime on the orders table with: ALTER PUBLICATION supabase_realtime ADD TABLE orders;
10. Do not fetch all columns — select only: id, customer_name, total_amount, status, created_at
Stack: Next.js 14, TypeScript, Supabase JS v2, Tailwind CSS
Why It Works
Every requirement in that prompt is a scar from a real production incident. Let me walk through the important ones.
Requirement 1 — Client Component only. Supabase Realtime uses WebSockets. WebSockets are a browser API. Server Components have no browser. This sounds obvious, but it's one of the most common errors in the wild, documented explicitly in Supabase's own troubleshooting guides. If you let an AI generate a Realtime subscription inside an async Server Component, it fails silently or throws cryptic errors at runtime.
Requirement 5 — supabase.removeChannel(channel). This is the exact fix my mentee's restaurant app needed. The modern Supabase JS v2 API uses removeChannel (not the older unsubscribe()). Forgetting this creates zombie subscriptions. According to production case studies of Supabase Realtime gotchas, apps that skip cleanup see memory growth that crashes the browser tab after a few hours of use—catastrophic for any ops dashboard meant to run all day.
Requirement 7 — useRef for subscription state. React 18 StrictMode runs effects twice in development to surface side effects. Without a ref guard, you get two active subscriptions and every database event fires twice in your UI—doubled order counts, doubled revenue totals. The fix is a single line, but an AI that doesn't know to include it will produce code that appears to work in production (where StrictMode is off) but misbehaves immediately when you run it locally.
Requirement 9 — Enable Realtime explicitly. Supabase disables Realtime on all tables by default, for performance and security reasons. This is the most common "why isn't my subscription working" issue. The prompt bakes the SQL command directly into the output so the generated code includes the migration step, not just the frontend code.
Here's the core subscription pattern the prompt should produce:
'use client'
import { useEffect, useRef, useState } from 'react'
import { createClient } from '@/utils/supabase/client'
type Order = {
id: string
customer_name: string
total_amount: number
status: string
created_at: string
}
export default function LiveOrdersDashboard({ initialOrders }: { initialOrders: Order[] }) {
const [orders, setOrders] = useState<Order[]>(initialOrders)
const [isLive, setIsLive] = useState(false)
const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null)
const supabase = createClient()
useEffect(() => {
// Guard against duplicate subscriptions (React StrictMode)
if (channelRef.current) return
const channel = supabase
.channel('orders-dashboard')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'orders' },
(payload) => {
setOrders((current) => {
if (payload.eventType === 'INSERT') {
return [payload.new as Order, ...current].slice(0, 10)
}
if (payload.eventType === 'UPDATE') {
return current.map((o) => (o.id === payload.new.id ? (payload.new as Order) : o))
}
if (payload.eventType === 'DELETE') {
return current.filter((o) => o.id !== payload.old.id)
}
return current
})
}
)
.subscribe((status) => {
setIsLive(status === 'SUBSCRIBED')
})
channelRef.current = channel
return () => {
supabase.removeChannel(channel)
channelRef.current = null
}
}, [])
const revenue = orders.reduce((sum, o) => sum + o.total_amount, 0)
return (
<div className="p-6">
<div className="flex items-center gap-2 mb-4">
<span className={`h-2 w-2 rounded-full ${isLive ? 'bg-green-500' : 'bg-yellow-400 animate-pulse'}`} />
<span className="text-sm text-gray-500">{isLive ? 'Live' : 'Reconnecting...'}</span>
</div>
<div className="grid grid-cols-2 gap-4 mb-6">
<div className="bg-white rounded-xl p-4 shadow">
<p className="text-gray-500 text-sm">Orders</p>
<p className="text-3xl font-bold">{orders.length}</p>
</div>
<div className="bg-white rounded-xl p-4 shadow">
<p className="text-gray-500 text-sm">Revenue</p>
<p className="text-3xl font-bold">${revenue.toFixed(2)}</p>
</div>
</div>
<ul className="space-y-2">
{orders.map((order) => (
<li key={order.id} className="flex justify-between bg-white rounded-lg p-3 shadow-sm">
<span>{order.customer_name}</span>
<span className="font-medium">${order.total_amount.toFixed(2)}</span>
</li>
))}
</ul>
</div>
)
}
And the SQL migration to run first:
-- Enable realtime on the orders table
ALTER PUBLICATION supabase_realtime ADD TABLE orders;
-- RLS: allow authenticated users to read all orders (adjust for your access model)
CREATE POLICY "Authenticated users can view orders"
ON orders FOR SELECT
TO authenticated
USING (true);
The Anti-Prompt
Here's how most vibe coders actually prompt this:
"Make a real-time dashboard using Supabase that shows orders live."
This fails in at least three ways:
- No cleanup instruction → The AI skips the
removeChannelreturn, creating memory leaks and zombie connections that kill your app under real load. - No Server vs. Client guidance → The AI may generate a Server Component with a Realtime subscription, which either throws an error or silently does nothing.
- No column selection → The AI fetches all columns including any large
notes,metadata, orjsonfields, sending megabytes over WebSocket on every order update. As documented in production Supabase apps, large payloads are a common cause of sluggish dashboards that nobody can diagnose.
The AI isn't wrong for producing this output—it's giving you exactly what you asked for. The prompt is what fails.
Variations
Once your core dashboard works, extend the prompt with one of these targeted additions:
Multi-table monitoring:
Add a second channel that listens to the 'inventory' table for UPDATE events.
When stock drops below 5 units, surface a low-stock badge on the dashboard.
Use a single named channel per table to avoid conflicts.
Presence: show who's viewing the dashboard:
Add Supabase Presence to track how many browser sessions are currently
viewing this dashboard. Display a "3 viewers" badge in the header.
Use channel.track() on subscribe and channel.on('presence', ...)
to update the count.
Reconnection + data sync:
When the connection status changes from SUBSCRIBED to any other state,
re-fetch the last 10 orders from the server to fill any gaps that occurred
while the WebSocket was disconnected. Do not rely solely on real-time events
for data correctness.
That last variation is the most important one for production. WebSocket connections drop. Your dashboard state and your database can diverge during an outage. Always have a recovery fetch.
Checklist Before You Ship
-
ALTER PUBLICATION supabase_realtime ADD TABLE orders;— run this migration or subscriptions will silently fail - Component has
'use client'at the top -
supabase.removeChannel(channel)is in theuseEffectcleanup return -
useRefguards against duplicate subscriptions in development - Only selected columns streamed (not
SELECT *) - RLS policy allows authenticated access to the subscribed table
- Connection status indicator visible in the UI
- Recovery fetch on reconnect for data correctness
- Tested with browser DevTools → Network → throttled to "Slow 3G" to simulate drops
Ask The Guild
What's the gnarliest real-time bug you've hit in production? Memory leak, silent RLS failure, duplicate events from StrictMode—we've all got a war story. Drop it in #prompt-of-the-day in the Guild Discord. Bonus points if you share the prompt that finally fixed it.