Skip to content
Prompt of the Day — Part 6 of 30

Prompt of the Day: Build a Real-Time Dashboard with Supabase

Written by claude-sonnet-4 · Edited by claude-sonnet-4
supabaserealtimereactnextjsdashboardwebsocketstypescriptpostgresvibe-codingprompt-of-the-day

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:

  1. No cleanup instruction → The AI skips the removeChannel return, creating memory leaks and zombie connections that kill your app under real load.
  2. 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.
  3. No column selection → The AI fetches all columns including any large notes, metadata, or json fields, 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 the useEffect cleanup return
  • useRef guards 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.

Copy A Prompt Next

Review and debug

If this article changed how you think about the problem, copy a prompt that turns that judgment into one safe, reviewable next step.

Matching public prompts

23

Keep the task scoped, copy the prompt, then inspect one reviewable diff before the agent continues.

Need the safest first move instead? Open the curated sample prompts before you browse the broader library.

Working With AI ToolsWorking With AI Tools

System Prompts — .cursorrules and CLAUDE.md Explained

Write system prompts that give AI persistent context about your project and preferences.

Preview
**Use this when you want the agent to draft your persistent project instructions:**
"Help me write a system prompt file for this project.
Tool target: [Cursor / Claude Code / both]
Project summary: [what the app does]
Stack: [frameworks, languages, key services]
Prompt Engineering

Turn this workflow advice into a durable operating system

Prompt and workflow posts are the quick win. The learning paths turn them into a durable operating model for tools, prompts, and agent supervision.

Best Next Path

Working With AI Tools

Explorer · Free

Turn ad hoc prompting into a repeatable workflow with better tool choice, stronger prompting, and safer day-to-day AI habits.

23 lessonsIncluded in the free Explorer plan

Need the free route first?

Start with Foundations for AI-Assisted Builders if you want the workflow and vocabulary before you dive into the deeper path above.

T

About Tom Hundley

Tom Hundley writes for builders who need stronger technical judgment around AI-assisted software work. The Guild turns production experience into public articles, copy-paste prompts, and structured learning paths that help non-software developers supervise AI agents more safely.

Do this next

Leave this article with one concrete move. Copy the matching prompt, or start with the path that teaches the safest next skill in sequence.