SemiLayerDocs

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.