ExamplesLive Order Dashboard

Live Order Dashboard

Subscribe to a D1 table and watch changes arrive in real time — no sdk.socket.emit() needed. When the server runs a SQL UPDATE, ChangeTracker detects it and broadcasts the row to all subscribers automatically.

Demonstrates: D1 table subscriptions, channel('orders') (no slash = ChangeTracker), on('INSERT'), on('UPDATE')

Source: examples/socket-hub/src/examples/LiveOrderDashboard/


How it works

Server runs: UPDATE orders SET status = 'shipped' WHERE id = '123'
  → ChangeTracker polls D1 every 200ms
  → Detects the mutation
  → Broadcasts UPDATE event to all subscribers on channel('orders')
  → Dashboard UI updates in real time — no manual emit required

The key difference from pub/sub: the channel name has no slash. This activates ChangeTracker for the orders table.

Client code

import { useAerostack } from '@aerostack/react'
import { useEffect, useState } from 'react'
 
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
 
interface Order {
  id: string
  customer_name: string
  product: string
  amount: number
  status: OrderStatus
  created_at: number
}
 
const STATUS_COLORS: Record<OrderStatus, string> = {
  pending:    'bg-yellow-500/10 text-yellow-400',
  processing: 'bg-blue-500/10 text-blue-400',
  shipped:    'bg-purple-500/10 text-purple-400',
  delivered:  'bg-green-500/10 text-green-400',
  cancelled:  'bg-red-500/10 text-red-400',
}
 
export function LiveOrderDashboard() {
  const { realtime } = useAerostack()
  const [orders, setOrders] = useState<Order[]>([])
  const [recentUpdate, setRecentUpdate] = useState<string | null>(null)
 
  useEffect(() => {
    // Load initial data
    fetch('/api/examples/orders')
      .then(r => r.json())
      .then(data => setOrders(data.orders ?? []))
 
    // Subscribe to live changes
    // Note: no slash in topic name → ChangeTracker activates for 'orders' table
    const channel = realtime.channel('orders')
 
    channel
      .on('INSERT', ({ data }) => {
        setOrders(prev => [data, ...prev])
        setRecentUpdate(`New order: ${data.id}`)
        setTimeout(() => setRecentUpdate(null), 3000)
      })
      .on('UPDATE', ({ data }) => {
        setOrders(prev => prev.map(o => o.id === data.id ? { ...o, ...data } : o))
        setRecentUpdate(`Order ${data.id} → ${data.status}`)
        setTimeout(() => setRecentUpdate(null), 3000)
      })
      .subscribe()
 
    return () => channel.unsubscribe()
  }, [realtime])
 
  const updateOrderStatus = async (orderId: string, status: OrderStatus) => {
    await fetch('/api/examples/orders/update', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ orderId, status }),
    })
    // No need to update local state — the subscription will receive the UPDATE event
  }
 
  return (
    <div className="p-6">
      <div className="flex items-center justify-between mb-6">
        <h1 className="text-2xl font-bold text-white">Orders</h1>
        {recentUpdate && (
          <div className="text-sm text-green-400 bg-green-500/10 px-4 py-2 rounded-full">
            ↻ {recentUpdate}
          </div>
        )}
      </div>
 
      <div className="space-y-3">
        {orders.map(order => (
          <div
            key={order.id}
            className="flex items-center justify-between p-4 rounded-xl border border-gray-800 bg-gray-900/50"
          >
            <div>
              <div className="font-medium text-white">{order.customer_name}</div>
              <div className="text-sm text-gray-400">{order.product} · ${order.amount}</div>
            </div>
            <div className="flex items-center gap-3">
              <span className={`text-xs px-3 py-1 rounded-full font-medium ${STATUS_COLORS[order.status]}`}>
                {order.status}
              </span>
              <select
                value={order.status}
                onChange={e => updateOrderStatus(order.id, e.target.value as OrderStatus)}
                className="text-xs bg-gray-800 border border-gray-700 rounded-lg px-2 py-1 text-gray-300"
              >
                <option value="pending">pending</option>
                <option value="processing">processing</option>
                <option value="shipped">shipped</option>
                <option value="delivered">delivered</option>
                <option value="cancelled">cancelled</option>
              </select>
            </div>
          </div>
        ))}
      </div>
    </div>
  )
}

Server code

import { Hono } from 'hono'
import { sdk } from '@aerostack/sdk'
 
const app = new Hono()
 
// Setup: create orders table
app.post('/setup', async (c) => {
  await sdk.db.exec(`
    CREATE TABLE IF NOT EXISTS orders (
      id TEXT PRIMARY KEY,
      customer_name TEXT NOT NULL,
      product TEXT NOT NULL,
      amount REAL NOT NULL,
      status TEXT DEFAULT 'pending',
      created_at INTEGER NOT NULL
    )
  `)
  return c.json({ ok: true })
})
 
// List orders
app.get('/examples/orders', async (c) => {
  const { results } = await sdk.db.query(
    'SELECT * FROM orders ORDER BY created_at DESC LIMIT 50'
  )
  return c.json({ orders: results })
})
 
// Update order status — ChangeTracker broadcasts the UPDATE automatically
app.post('/examples/orders/update', async (c) => {
  const { orderId, status } = await c.req.json()
 
  await sdk.db.query(
    'UPDATE orders SET status = ? WHERE id = ?',
    [status, orderId]
  )
 
  // No sdk.socket.emit needed — ChangeTracker handles the broadcast
  return c.json({ ok: true })
})
 
export default app

ChangeTracker polls D1 every 200ms. For very high-frequency updates, consider debouncing on the client side to avoid UI thrashing.