Example: Live Feed
Stream every insert, update, and delete from a lens in real time. This example builds a live activity feed using SemiLayer's WebSocket streaming and React.
Schema
// sl.config.ts
import { defineConfig } from '@semilayer/core'
export default defineConfig({
stack: 'my-app',
sources: {
'main-db': { bridge: '@semilayer/bridge-postgres' },
},
lenses: {
orders: {
source: 'main-db',
table: 'public.orders',
primaryKey: 'id',
fields: {
id: { type: 'number', primaryKey: true },
status: { type: 'text' },
total: { type: 'number' },
customerId: { type: 'number', from: 'customer_id' },
createdAt: { type: 'date', from: 'created_at' },
updatedAt: { type: 'date', from: 'updated_at' },
},
facets: {
search: { fields: ['status'] },
},
syncInterval: '1m',
},
},
})
semilayer push --resume-ingest
semilayer generate
Live Feed Hook
// hooks/useLiveFeed.ts
import { useEffect, useRef, useState } from 'react'
import { createBeam } from '@/generated/semilayer'
const beam = createBeam({
baseUrl: process.env.NEXT_PUBLIC_SEMILAYER_BASE_URL!,
apiKey: process.env.NEXT_PUBLIC_SEMILAYER_KEY!, // pk_live_...
})
export interface FeedEvent {
id: string
kind: 'insert' | 'update' | 'delete'
record: ReturnType<typeof beam.orders.stream.subscribe> extends AsyncIterable<infer E> ? E['record'] : never
receivedAt: Date
}
export function useLiveFeed(maxEvents = 50) {
const [events, setEvents] = useState<FeedEvent[]>([])
const [connected, setConnected] = useState(false)
const abortRef = useRef<AbortController | null>(null)
useEffect(() => {
const controller = new AbortController()
abortRef.current = controller
async function subscribe() {
setConnected(true)
try {
for await (const event of beam.orders.stream.subscribe()) {
if (controller.signal.aborted) break
setEvents(prev => [
{ id: crypto.randomUUID(), kind: event.kind, record: event.record, receivedAt: new Date() },
...prev.slice(0, maxEvents - 1),
])
}
} catch {
// reconnect on error
if (!controller.signal.aborted) {
setTimeout(subscribe, 2000)
}
} finally {
setConnected(false)
}
}
subscribe()
return () => controller.abort()
}, [maxEvents])
return { events, connected }
}
Live Feed Component
// components/LiveOrderFeed.tsx
'use client'
import { useLiveFeed } from '@/hooks/useLiveFeed'
const kindColor = {
insert: '#22c55e',
update: '#3b82f6',
delete: '#ef4444',
}
const kindLabel = {
insert: 'New order',
update: 'Updated',
delete: 'Cancelled',
}
export function LiveOrderFeed() {
const { events, connected } = useLiveFeed(100)
return (
<div style={{ fontFamily: 'monospace', fontSize: 14 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
<span style={{
width: 8, height: 8, borderRadius: '50%',
background: connected ? '#22c55e' : '#ef4444',
display: 'inline-block',
}} />
<span style={{ color: '#666' }}>
{connected ? 'Live' : 'Reconnecting...'}
</span>
<span style={{ color: '#999', marginLeft: 'auto' }}>
{events.length} events
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
{events.map(event => (
<div
key={event.id}
style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '8px 12px',
background: '#f9fafb',
borderRadius: 6,
borderLeft: `3px solid ${kindColor[event.kind]}`,
animation: 'fadeIn 0.2s ease',
}}
>
<span style={{ color: kindColor[event.kind], fontWeight: 600, minWidth: 80 }}>
{kindLabel[event.kind]}
</span>
<span style={{ color: '#374151' }}>
#{event.record.id} — ${event.record.total}
</span>
<span style={{ color: '#9ca3af', marginLeft: 'auto', fontSize: 12 }}>
{event.record.status}
</span>
<span style={{ color: '#d1d5db', fontSize: 11 }}>
{event.receivedAt.toLocaleTimeString()}
</span>
</div>
))}
</div>
{events.length === 0 && (
<p style={{ color: '#9ca3af', textAlign: 'center', padding: '32px 0' }}>
Waiting for activity...
</p>
)}
</div>
)
}
Observe a Single Record
Track one order through its lifecycle:
// components/OrderStatus.tsx
'use client'
import { useEffect, useState } from 'react'
import { createBeam } from '@/generated/semilayer'
const beam = createBeam({
baseUrl: process.env.NEXT_PUBLIC_SEMILAYER_BASE_URL!,
apiKey: process.env.NEXT_PUBLIC_SEMILAYER_KEY!,
})
export function OrderStatus({ orderId }: { orderId: string }) {
const [order, setOrder] = useState<{ status: string; total: number } | null>(null)
useEffect(() => {
const controller = new AbortController()
async function observe() {
for await (const snapshot of beam.orders.observe(orderId)) {
if (controller.signal.aborted) break
setOrder({ status: snapshot.status, total: snapshot.total })
}
}
observe()
return () => controller.abort()
}, [orderId])
if (!order) return <span>Loading...</span>
return (
<div>
<strong>Order #{orderId}</strong>
<div>Status: {order.status}</div>
<div>Total: ${order.total}</div>
</div>
)
}
Direct HTTP WebSocket
No generated client? Connect directly:
const ws = new WebSocket(
'wss://api.semilayer.com/v1/stream/orders' +
'?apiKey=pk_live_...'
)
ws.onopen = () => {
ws.send(JSON.stringify({ id: '1', op: 'subscribe' }))
}
ws.onmessage = (e) => {
const frame = JSON.parse(e.data)
if (frame.kind === 'event') {
console.log(frame.kind, frame.record) // 'insert' | 'update' | 'delete'
}
}
See HTTP & WebSocket for the full frame protocol.