SemiLayerDocs

Bridge SDK

A Bridge is a data source adapter. It tells SemiLayer how to connect to a specific source, read records in pages, and count them. Bridges are the only place SemiLayer touches your data — and it only ever reads.

SemiLayer ships first-party bridges for 20+ data sources — every major relational, document, key-value, analytics, and search engine. They all live in the semilayer/bridge-sdk monorepo alongside the SDK itself and the contribution tooling. See the Ecosystem section below for the full list.


The Bridge Interface

Every bridge must implement this interface, defined in @semilayer/core (re-exported from @semilayer/bridge-sdk):

import type { Bridge, BridgeRow, ReadOptions, ReadResult, QueryOptions, QueryResult } from '@semilayer/bridge-sdk'

export class MyBridge implements Bridge {
  constructor(config: Record<string, unknown>) {}

  /** Open the connection. Called once before ingest begins. */
  async connect(): Promise<void> { ... }

  /**
   * Read one page of records from a target (table, collection, endpoint, etc.).
   *
   * options.cursor     — opaque string from the previous page's result.nextCursor.
   *                      Absent on the first call.
   * options.limit      — max records per page. Defaults to 1000 if omitted.
   * options.fields     — source column/field names to return. Return all if omitted.
   * options.changedSince — only return records updated after this Date.
   *                       Requires options.changeTrackingColumn to be set.
   *
   * Returns:
   *   rows:       array of { [field]: value } records
   *   nextCursor: opaque string for the next page. undefined when exhausted.
   *   total:      optional total record count for progress reporting.
   */
  async read(target: string, options?: ReadOptions): Promise<ReadResult> { ... }

  /** Return the total number of records in target. Used for progress reporting. */
  async count(target: string): Promise<number> { ... }

  /** Close the connection. Called after ingest completes or errors. */
  async disconnect(): Promise<void> { ... }

  /**
   * Optional: execute a WHERE-filtered query directly on the source.
   * Required to enable lens.rules.query. Not needed for search or similar.
   */
  async query?(
    target: string,
    options: QueryOptions,
  ): Promise<QueryResult<BridgeRow>> { ... }

  /**
   * Optional: list available targets (tables, collections, endpoints, ...).
   * Used by `semilayer sources connect` introspection.
   */
  async listTargets?(): Promise<string[]>

  /**
   * Optional: return schema info for a target (column names, types, PK).
   * Used by `semilayer sources connect` introspection.
   */
  async introspectTarget?(target: string): Promise<TargetSchema>
}

Key Types

type BridgeRow = Record<string, unknown>

interface ReadOptions {
  fields?: string[]            // column names to select; all if omitted
  cursor?: string              // opaque pagination cursor from previous page
  limit?: number               // page size; defaults to 1000
  changedSince?: Date          // incremental: only rows updated after this
  changeTrackingColumn?: string // column to compare against changedSince
}

interface ReadResult {
  rows: BridgeRow[]
  nextCursor?: string          // undefined = last page
  total?: number               // optional — for progress display
}

interface QueryOptions {
  where?: Record<string, unknown>                          // field equality or operators
  orderBy?: { field: string; dir?: 'asc' | 'desc' }       // single or array
        | Array<{ field: string; dir?: 'asc' | 'desc' }>
  select?: string[]            // fields to return; all if omitted
  limit?: number
  offset?: number
}

interface QueryResult<T = BridgeRow> {
  rows: T[]
  total?: number
}

Compliance Test Suite

Every bridge should pass the compliance test suite provided by @semilayer/bridge-sdk. The suite verifies the Bridge contract: connect/disconnect, read pagination, cursor behavior, count, and (if implemented) query.

import { createBridgeTestSuite } from '@semilayer/bridge-sdk'
import { describe } from 'vitest'
import { MyBridge } from '../src/bridge.js'

describe('MyBridge compliance', () => {
  createBridgeTestSuite({
    factory: () => new MyBridge({ url: process.env.MY_SOURCE_URL }),
    seed: {
      target: 'test_records',
      primaryKey: 'id',
      rows: [
        { id: 1, name: 'Alpha' },
        { id: 2, name: 'Beta' },
        { id: 3, name: 'Gamma' },
      ],
    },
    // Optional lifecycle hooks for test setup/teardown:
    beforeSeed: async (bridge) => {
      // e.g. create the test table before seeding
    },
    afterCleanup: async (bridge) => {
      // e.g. drop the test table after tests finish
    },
  })
})

The BridgeTestSuiteOptions shape:

FieldTypeDescription
factory() => BridgeFactory called before each test group
seed.targetstringTarget name (table, collection, etc.)
seed.rowsBridgeRow[]Records the test suite will read and verify
seed.primaryKeystringPK field name used for cursor overlap checks
beforeSeed?(bridge) => Promise<void>Called after connect, before data is verified
afterCleanup?(bridge) => Promise<void>Called after disconnect for teardown

Run the suite with pnpm test. All tests must pass before a bridge is eligible for inclusion in the built-in registry.


MockBridge

MockBridge is an in-memory bridge for unit testing your application code without a real connection. It fully implements the Bridge interface including query.

import { MockBridge } from '@semilayer/bridge-sdk'

const bridge = new MockBridge()

// Seed data before connecting
bridge.seed('products', [
  { id: 1, name: 'Sneakers', category: 'footwear', price: 89 },
  { id: 2, name: 'Boots',    category: 'footwear', price: 149 },
  { id: 3, name: 'T-Shirt',  category: 'apparel',  price: 29 },
])

await bridge.connect()

const { rows } = await bridge.read('products', { limit: 2 })
// rows → [{ id: 1, name: 'Sneakers', ... }, { id: 2, name: 'Boots', ... }]

const count = await bridge.count('products')
// count → 3

const result = await bridge.query('products', {
  where: { category: 'footwear' },
  orderBy: { field: 'price', dir: 'asc' },
})
// result.rows → [{ id: 1, ... }, { id: 2, ... }]

await bridge.disconnect()

Building a Bridge

1. Scaffold from the template

Clone the bridge monorepo and use the scaffold script:

git clone https://github.com/semilayer/bridge-sdk
cd bridge-sdk
pnpm install

# Creates packages/bridge-<name>/ from the template
pnpm new-bridge <name>

The script scaffolds packages/bridge-<name>/src/bridge.ts with interface stubs, creates a compliance test file, and registers the package in the release manifest.

2. Implement the interface

Open packages/bridge-<name>/src/bridge.ts and implement the four required methods: connect, read, count, disconnect. Add query if you want direct-source queries.

Tips:

  • Use cursor-based (keyset) pagination in read(). Offset-based pagination is unreliable on large sources with concurrent writes.
  • Keep the default connection pool size small (3–5). The worker may run many ingest jobs concurrently.
  • Validate config in connect(). Fail early with a clear error rather than mid-ingest.
  • count() is called for progress reporting. Keep it cheap if possible.

3. Run the compliance suite

# From the monorepo root:
pnpm --filter @semilayer/bridge-<name> test

All tests must pass before submitting.

4. Register in the built-in registry

Open lib/bridge-resolver/src/index.ts and add your bridge:

export const BUILT_IN_BRIDGES: Record<string, () => Promise<BridgeCtor>> = {
  '@semilayer/bridge-postgres': () =>
    import('@semilayer/bridge-postgres').then((m) => m.PostgresBridge),

  // Add your bridge:
  '@semilayer/bridge-<name>': () =>
    import('@semilayer/bridge-<name>').then((m) => m.MyBridge),
}

Add it as a workspace:* dependency in lib/bridge-resolver/package.json:

{
  "dependencies": {
    "@semilayer/bridge-<name>": "workspace:*"
  }
}

5. Submit a PR

Open a PR against semilayer/bridge-sdk. Include:

  • The new packages/bridge-<name>/ directory
  • The registry update in lib/bridge-resolver/
  • A passing CI run (GitHub Actions runs the compliance suite against a real source)
  • A brief description of the data source and any notable behaviors or limitations

Review SLA is 2 weeks. We'll leave feedback or merge.


Release Pipeline

Merging the PR triggers release-please to open a release PR. Merging the release PR publishes both @semilayer/bridge-<name> and a bumped @semilayer/bridge-resolver to npm atomically. The node-workspace plugin handles the version ripple automatically.

Once @semilayer/bridge-resolver is published with your bridge, deployments that bump their bridge-resolver dependency pick it up automatically.


Private & Enterprise Bridges

Enterprise deployments that need a private bridge (proprietary source, internal code) use runtime registration rather than the public registry:

// apps/worker/src/bridges.ts (enterprise custom)
import { registerBridge } from '@semilayer/bridge-resolver'
import { MyInternalBridge } from '@my-company/bridge-internal'

registerBridge('@my-company/bridge-internal', MyInternalBridge)

Call registerBridge at worker startup, before the first ingest job runs. The bridge is available for that deployment only — the public registry is unaffected.


Reference Implementation

@semilayer/bridge-postgres is the reference implementation. It's the most complete bridge and the best one to study when building your own.

  • Location: packages/bridge-postgres/ in semilayer/bridge-sdk
  • Connection pooling: pg.Pool with max: 3
  • Pagination: keyset pagination via primary key (WHERE pk > $cursor ORDER BY pk ASC)
  • Incremental: changedSince + changeTrackingColumn support
  • Query: full WHERE, ORDER BY, LIMIT, OFFSET support with operators ($eq, $gt, $gte, $lt, $lte, $in)
  • Introspection: listTargets() and introspectTarget() for Console source setup

Ecosystem

SemiLayer ships first-party bridges across every major category. All of them live in the semilayer/bridge-sdk monorepo and are published to npm under the @semilayer/ scope.

Relational SQL

BridgePackage
PostgreSQL@semilayer/bridge-postgres
MySQL@semilayer/bridge-mysql
MariaDB@semilayer/bridge-mariadb
SQLite@semilayer/bridge-sqlite
Microsoft SQL Server@semilayer/bridge-mssql
Oracle@semilayer/bridge-oracle
CockroachDB@semilayer/bridge-cockroachdb

Cloud / Edge SQL

BridgePackage
Neon@semilayer/bridge-neon
Turso@semilayer/bridge-turso
PlanetScale@semilayer/bridge-planetscale
Cloudflare D1@semilayer/bridge-d1
Supabase@semilayer/bridge-supabase

Document

BridgePackage
MongoDB@semilayer/bridge-mongodb
Firestore@semilayer/bridge-firestore

Key-Value

BridgePackage
Redis@semilayer/bridge-redis
Upstash@semilayer/bridge-upstash

AWS

BridgePackage
DynamoDB@semilayer/bridge-dynamodb

Analytics

BridgePackage
ClickHouse@semilayer/bridge-clickhouse
BigQuery@semilayer/bridge-bigquery
DuckDB@semilayer/bridge-duckdb
Snowflake@semilayer/bridge-snowflake

Search / Wide-column

BridgePackage
Elasticsearch@semilayer/bridge-elasticsearch
Cassandra@semilayer/bridge-cassandra

Don't see your data source? See the scaffold instructions above and open a PR. We welcome bridges for any data source — SQL databases, NoSQL stores, REST APIs, file systems, message queues, or anything you can read from.