ExamplesRealtime Chat

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 event

Client 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 app

Setup

cd examples/socket-hub/server
npm run dev
 
# Run setup to create tables
curl -X POST http://localhost:8787/setup

Then 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().