SemiLayerDocs

Beam Client

The @semilayer/client package provides BeamClient — the canonical SemiLayer runtime client. You can use it directly or via the generated Beam wrapper created by semilayer generate.

Installation

npm install @semilayer/client

BeamClient

Constructor

import { BeamClient, type BeamConfig } from '@semilayer/client'

const client = new BeamClient({
  baseUrl: 'https://api.semilayer.com',  // required
  apiKey: 'sk_live_...',                 // required
  userToken?: string,                    // optional — user JWT for RBAC
  wsUrl?: string,                        // optional — override WebSocket URL
  fetch?: typeof fetch,                  // optional — custom fetch implementation
  WebSocket?: WebSocketConstructor,      // optional — custom WebSocket class (Node.js)
})

withUser(userToken: string): BeamClient

Returns a new BeamClient bound to a user JWT. Cheap — shares the same connection and config, only differs in the X-User-Token header / ?userToken= parameter it attaches.

const sharedClient = new BeamClient({ baseUrl, apiKey: 'pk_live_...' })

// Per request — bind the user's JWT:
const userClient = sharedClient.withUser(req.headers['x-user-token'])
const { results } = await userClient.search('products', { query: 'shoes' })

HTTP Methods

These methods always use HTTP (no WebSocket).

search(lens, params)

async search<M = Record<string, unknown>>(
  lens: string,
  params: SearchParams,
): Promise<SearchResponse<M>>
interface SearchParams {
  query: string
  limit?: number      // default: 10
  minScore?: number   // default: 0
  mode?: 'semantic' | 'keyword' | 'hybrid'
}

interface SearchResponse<M> {
  results: SearchResult<M>[]
  meta: { lens: string; query: string; mode: string; count: number; durationMs: number }
}

interface SearchResult<M> {
  id: string           // internal embedding ID
  sourceRowId: string  // your source DB primary key
  content: string | null
  metadata: M          // your declared fields
  score: number        // 0-1 cosine similarity
}

Example:

interface Product { id: number; name: string; category: string; price: number }

const { results } = await client.search<Product>('products', {
  query: 'eco-friendly water bottle',
  limit: 10,
  mode: 'hybrid',
})

// results[0].metadata.name   ← string ✓
// results[0].score           ← number ✓

similar(lens, params)

async similar<M = Record<string, unknown>>(
  lens: string,
  params: SimilarParams,
): Promise<SimilarResponse<M>>
interface SimilarParams {
  id: string          // source record primary key
  limit?: number
  minScore?: number
}

interface SimilarResponse<M> {
  results: SearchResult<M>[]  // same shape as SearchResponse
  meta: { lens: string; sourceId: string; count: number; durationMs: number }
}

Example:

const { results } = await client.similar<Product>('products', { id: '42', limit: 5 })
// results[0].metadata.name  ← name of similar product

query(lens, params?)

async query<M = Record<string, unknown>>(
  lens: string,
  params?: QueryParams,
): Promise<QueryResponse<M>>
interface QueryParams {
  where?: Record<string, unknown>
  orderBy?: { field: string; dir?: 'asc' | 'desc' }
          | Array<{ field: string; dir?: 'asc' | 'desc' }>
  limit?: number
  offset?: number
  cursor?: string
  select?: string[]
}

interface QueryResponse<M> {
  rows: M[]  // direct M access — no .metadata wrapper
  meta: { lens: string; total?: number; nextCursor?: string; count: number; durationMs: number }
}

Example:

const { rows, meta } = await client.query<Product>('products', {
  where: { category: 'footwear' },
  orderBy: { field: 'price', dir: 'asc' },
  limit: 20,
})

// rows[0].name  ← string (direct M access, no .metadata wrapper)

// Paginate:
if (meta.nextCursor) {
  const page2 = await client.query<Product>('products', {
    where: { category: 'footwear' },
    cursor: meta.nextCursor,
    limit: 20,
  })
}

Streaming Methods

These methods use a WebSocket connection. The socket is opened lazily on the first streaming call and reused across all subsequent calls on the same client instance.

stream.search(lens, params)

stream.search<M = Record<string, unknown>>(
  lens: string,
  params: StreamSearchParams,  // extends SearchParams + { batchSize?: number }
): AsyncIterable<SearchResult<M>>

Example:

for await (const result of client.stream.search<Product>('products', {
  query: 'eco-friendly water bottle',
  limit: 50,
})) {
  render(result) // SearchResult<Product> — arrives per batch before all results are ready
}

stream.query(lens, params)

stream.query<M = Record<string, unknown>>(
  lens: string,
  params: StreamQueryParams,  // extends QueryParams + { batchSize?: number }
): AsyncIterable<M>

Example:

for await (const row of client.stream.query<Product>('products', {
  where: { category: 'footwear' },
})) {
  process(row)  // M — direct field access
}

stream.subscribe(lens, params?)

stream.subscribe<M = Record<string, unknown>>(
  lens: string,
  params?: SubscribeParams,
): AsyncIterable<StreamEvent<M>>
interface SubscribeParams {
  filter?: Record<string, unknown>  // simple equality filter (v0.1)
}

interface StreamEvent<M> {
  kind: 'insert' | 'update' | 'delete'
  record: M
}

Example:

for await (const event of client.stream.subscribe<Product>('products')) {
  if (event.kind === 'insert') addToList(event.record)
  if (event.kind === 'update') updateInList(event.record)
  if (event.kind === 'delete') removeFromList(event.record)
}

observe(lens, recordId)

Observe a single record. First yield is the current record state; subsequent yields fire only when the record changes.

observe<M = Record<string, unknown>>(
  lens: string,
  recordId: string,
): AsyncIterable<M>

Example:

for await (const snapshot of client.observe<Product>('products', '42')) {
  setProduct(snapshot)  // M — fires on every change to this record
}

Error Types

import {
  BeamError,
  BeamAuthError,
  BeamNotFoundError,
  BeamStreamError,
  BeamStreamRateLimitError,
  BeamStreamClosedError,
} from '@semilayer/client'
ClassWhen thrown
BeamErrorHTTP 4xx/5xx (base class)
BeamAuthErrorHTTP 401 — invalid or missing API key
BeamNotFoundErrorHTTP 404 — lens not found
BeamStreamErrorWebSocket error frame received
BeamStreamRateLimitErrorRate limit on streaming (subclass of BeamStreamError)
BeamStreamClosedErrorWebSocket closed unexpectedly

Handling errors:

import { BeamAuthError, BeamStreamClosedError } from '@semilayer/client'

try {
  const { results } = await client.search('products', { query: 'shoes' })
} catch (err) {
  if (err instanceof BeamAuthError) {
    // API key invalid or expired
  }
  throw err
}

// Streaming — reconnect on close
async function watchFeed() {
  while (true) {
    try {
      for await (const event of client.stream.subscribe('products')) {
        handleEvent(event)
      }
    } catch (err) {
      if (err instanceof BeamStreamClosedError) {
        await sleep(1000)  // wait before reconnecting
        continue
      }
      throw err
    }
  }
}

Generated Beam Client

semilayer generate creates a typed wrapper around BeamClient based on your config.

import { createBeam, type Beam } from './semilayer'

const beam = createBeam({
  baseUrl: 'https://api.semilayer.com',
  apiKey: process.env.SEMILAYER_API_KEY!,
})

The generated Beam class has one property per Lens, each typed to your field declarations:

beam.products.search(params: SearchParams): Promise<SearchResponse<ProductsMetadata>>
beam.products.similar(params: SimilarParams): Promise<SimilarResponse<ProductsMetadata>>
beam.products.query(params?: QueryParams): Promise<QueryResponse<ProductsMetadata>>
beam.products.stream.search(params): AsyncIterable<SearchResult<ProductsMetadata>>
beam.products.stream.query(params): AsyncIterable<ProductsMetadata>
beam.products.stream.subscribe(params?): AsyncIterable<StreamEvent<ProductsMetadata>>
beam.products.observe(recordId: string): AsyncIterable<ProductsMetadata>

Where ProductsMetadata is generated from your field declarations:

// Generated: semilayer/metadata.ts
export interface ProductsMetadata {
  id: number
  name: string
  category: string
  price: number
}

Node.js Setup

In Node.js 18+, fetch and WebSocket are available globally. Earlier versions need polyfills:

import { BeamClient } from '@semilayer/client'
import WebSocket from 'ws'  // npm install ws

const client = new BeamClient({
  baseUrl: 'https://api.semilayer.com',
  apiKey: 'sk_live_...',
  WebSocket,  // pass the ws constructor
})