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 requiredThe 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 appChangeTracker polls D1 every 200ms. For very high-frequency updates, consider debouncing on the client side to avoid UI thrashing.