Realtime Chat
A simple channel-based chat room. Messages are sent via HTTP to the server, persisted to D1, and broadcast to all WebSocket subscribers.
Demonstrates: basic pub/sub, HTTP + WebSocket together, multi-tab sync
Source: examples/socket-hub/src/examples/RealtimeChat/
How it works
User types message
→ POST /examples/chat/send (HTTP)
→ Server: sdk.db.query INSERT INTO messages
→ Server: sdk.socket.emit('chat:message', message, roomId)
→ All subscribers on channel('general') receive the eventClient code
import { useAerostack } from '@aerostack/react'
import { useEffect, useRef, useState } from 'react'
interface Message {
id: string
userId: string
text: string
timestamp: number
}
export function RealtimeChat() {
const { realtime } = useAerostack()
const [messages, setMessages] = useState<Message[]>([])
const [inputText, setInputText] = useState('')
const bottomRef = useRef<HTMLDivElement>(null)
const roomId = 'general'
const userId = useRef(`user-${Math.random().toString(36).slice(2, 8)}`)
useEffect(() => {
const channel = realtime.channel(`chat/${roomId}`)
channel
.on('*', ({ data }) => {
if (data && data.text) {
setMessages(prev => {
// Avoid duplicate if we already added it optimistically
if (prev.find(m => m.id === data.id)) return prev
return [...prev, data]
})
}
})
.subscribe()
return () => channel.unsubscribe()
}, [realtime])
// Auto-scroll to bottom
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [messages])
const sendMessage = async () => {
if (!inputText.trim()) return
const message: Message = {
id: Date.now().toString(),
userId: userId.current,
text: inputText,
timestamp: Date.now(),
}
// Optimistic update
setMessages(prev => [...prev, message])
setInputText('')
// Send to server
await fetch('/api/examples/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ roomId, ...message }),
})
}
return (
<div className="flex flex-col h-screen max-w-2xl mx-auto p-4">
<div className="flex-1 overflow-y-auto space-y-2 mb-4">
{messages.map(msg => (
<div
key={msg.id}
className={`flex ${msg.userId === userId.current ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-xs px-4 py-2 rounded-2xl text-sm ${
msg.userId === userId.current
? 'bg-blue-600 text-white'
: 'bg-gray-800 text-gray-100'
}`}>
{msg.userId !== userId.current && (
<div className="text-xs text-gray-400 mb-1">{msg.userId}</div>
)}
{msg.text}
</div>
</div>
))}
<div ref={bottomRef} />
</div>
<form
onSubmit={e => { e.preventDefault(); sendMessage() }}
className="flex gap-2"
>
<input
value={inputText}
onChange={e => setInputText(e.target.value)}
placeholder="Type a message..."
className="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-2 text-white"
/>
<button
type="submit"
className="bg-blue-600 text-white px-6 py-2 rounded-xl font-medium"
>
Send
</button>
</form>
</div>
)
}Server code
import { Hono } from 'hono'
import { sdk } from '@aerostack/sdk'
const app = new Hono()
// Setup (run once): create tables
app.post('/setup', async (c) => {
await sdk.db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
room_id TEXT NOT NULL,
user_id TEXT NOT NULL,
text TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`)
return c.json({ ok: true })
})
// Send a chat message
app.post('/examples/chat/send', async (c) => {
const { roomId, id, userId, text, timestamp } = await c.req.json()
// Persist to DB
await sdk.db.query(
'INSERT INTO messages (id, room_id, user_id, text, created_at) VALUES (?, ?, ?, ?, ?)',
[id, roomId, userId, text, timestamp]
)
// Broadcast to all subscribers
sdk.socket.emit('chat:message', { id, userId, text, timestamp }, `chat/${roomId}`)
return c.json({ ok: true })
})
export default appSetup
cd examples/socket-hub/server
npm run dev
# Run setup to create tables
curl -X POST http://localhost:8787/setupThen open the client at http://localhost:5173 in multiple browser tabs.
The channel name chat/general uses a slash, so it’s a custom pub/sub channel — not a DB table subscription. Messages only arrive when your server explicitly calls sdk.socket.emit().