@broberg/db-sdk

中文文档
0.2.0发布于 2026/09/24FSL-1.1-Apache-2.0TypeScriptES Module近 30 天下载 6173

Thin shared database client for the broberg.ai fleet — one DbClient facade over libSQL/Turso and PostgreSQL/Supabase. The SQL dialect is passed through, not translated.

原始使用文档
文档目录10 节
  1. Install
  2. Use
  3. The escape hatch
  4. Embedded replicas (libSQL only)
  5. Supabase
  6. ORM
  7. API
  8. Migrating from 0.1.x
  9. Tests
  10. License

@broberg/db-sdk

The broberg.ai fleet's shared database client — one thin facade over two transports:

TransportForEnv default
libsql (default)Turso / libSQL, local file: databasesTURSO_DATABASE_URL, TURSO_AUTH_TOKEN
postgresPostgreSQL, including SupabaseDATABASE_URL

createClient() returns the same DbClient from both, so switching transport is a change at the call to createClient — for the connection.

The SQL is not translated. libSQL takes ? placeholders, Postgres takes $1, and the two dialects differ in DDL, functions and PRAGMA. The facade passes your SQL through untouched: rewriting ? into $1 would also rewrite a ? inside a string literal or a JSON operator. Moving an app between transports means moving its SQL too.

Install

bun add @broberg/db-sdk
# or: npm i @broberg/db-sdk

Use

import { createClient, createReplica, health } from '@broberg/db-sdk';

// Turso (reads TURSO_DATABASE_URL + TURSO_AUTH_TOKEN)
const db = createClient();

// Postgres / Supabase (reads DATABASE_URL)
const pg = createClient({ backend: 'postgres' });

await pg.execute({ sql: 'insert into notes (id, body) values ($1, $2)', args: [1, 'hej'] });
const { rows } = await pg.execute('select id, body from notes');
// rows: [{ id: 1, body: 'hej' }]

// Several statements in ONE transaction — all apply or none do.
await db.batch([
  { sql: 'delete from s where edge = ?', args: ['m1'] },
  { sql: 'insert into s (edge, name) values (?, ?)', args: ['m1', 'x'] },
], 'write');

await health(pg); // { ok: true, transport: 'postgres', version: '16.4' }

health() reports the transport that answered — read it there rather than inferring it from your own config.

The escape hatch

client.raw is the underlying driver's own client, typed per transport, for anything the facade does not cover:

const pg = createClient({ backend: 'postgres' });
await pg.raw.listen('events', (payload) => { /* … */ }); // postgres.js

const db = createClient();
await db.raw.executeMultiple(script); // @libsql/client

Embedded replicas (libSQL only)

A local SQLite file kept in sync with a remote Turso primary. There is no Postgres counterpart, and the config type says so — backend: 'postgres' on a ReplicaConfig is a compile error.

const edge = createReplica({ path: '/data/buddy.db', syncIntervalSeconds: 30 });
await edge.sync(); // or rely on the interval

Supabase

Use the pooler connection string as DATABASE_URL. Prepared statements are always off in the Postgres transport, because of what they do to Supabase's transaction-mode pooler (port 6543). Measured 23 Sep 2026 against a real Supabase project (eu-north-1), 40 concurrent queries with prepared statements ON:

RouteResult
transaction pooler, 6543hung after 5–8 answers — no error, 3 runs of 3
session pooler40/40
direct, 543240/40

It does not fail — it hangs, and only under concurrency, so a sequential test and a local Postgres both stay green. The contract suite runs 60 concurrent queries through the real pooler against a 20-second deadline, and turning prepared statements back on turns it red with HUNG.

Keep the service-role connection string on the server. Supabase auth belongs in @broberg/auth, storage in @broberg/media; this package is the SQL connection only.

ORM

The ORM stays in the consumer. Drizzle takes the driver's client, so hand it raw:

import { drizzle } from 'drizzle-orm/postgres-js';
const orm = drizzle(createClient({ backend: 'postgres' }).raw, { schema });

API

ExportSignature
createClient(cfg?: LibsqlConfig) => DbClient<LibsqlClient> · (cfg: PostgresConfig) => DbClient<Sql>
createReplica(cfg: ReplicaConfig) => ReplicaClient — libSQL only
health(client) => Promise<{ ok, transport, version }> — throws if unreachable

DbClient: transport, raw, execute(stmt), batch(stmts, mode?), close(). A statement is string or { sql, args? }. A result is { rows, columns, rowsAffected }, with rows as plain objects keyed by column.

Values are the driver's own — the facade unifies the shape, not the types. Measured on the same query:

SQL valuelibsqlpostgres
count(*), bigintnumberstring ('1')
numeric / 1.5 literalnumberstring ('1.5')
integernumbernumber
timestamptzstringDate
jsonbstringparsed object

Postgres returns int8 and numeric as strings because a JS number cannot hold an int8 above 2^53, and the facade does not convert them for the same reason — a silent precision loss is worse than a visible string. Wrap with Number(...) where you know the range is safe. The contract suite pins these, so a driver changing them turns it red.

batch modes are 'write' and 'deferred'. There is no 'read' mode: a libSQL read batch against a local file was measured executing an INSERT that Postgres refused, and a read-only promise that holds on one transport only is not one the facade can make.

Migrating from 0.1.x

createClient() used to return @libsql/client's own Client. It now returns DbClient. What changed for a caller:

  • execute(string | { sql, args }) and batch(stmts, 'write') — unchanged.
  • rows[i]['column'] — unchanged. Positional access rows[i][0] is gone: rows are plain objects now.
  • lastInsertRowid — gone from the facade (it is a SQLite concept). Use client.raw.execute(...), or returning id.
  • Anything else on the libSQL client (executeMultiple, transaction, sync): reach it via client.raw.
  • Drizzle: drizzle(createClient(), { schema }) — the pattern this README used to show — no longer works, because drizzle wants the driver's client. Write drizzle(createClient().raw, { schema }). Measured working for both transports (drizzle-orm/libsql and drizzle-orm/postgres-js).
  • type Client still exists as a deprecated alias of DbClient.

buddy (apps/server), the one known 0.1.x consumer, typechecks and passes its own db-sdk test against this version with no source changes.

Tests

DB_SDK_TEST_PG_URL=postgres://… bun test

One contract suite runs the same cases against libSQL (a local file), a real Postgres, and — when DB_SDK_TEST_PG_POOLER_URL is set — a real Supabase transaction pooler. Without DB_SDK_TEST_PG_URL the suite fails rather than skipping Postgres — a skipped transport reads as a green one. CI provides a Postgres service container.

License

FSL-1.1-Apache-2.0 © Christian Broberg

版本与发布记录

@broberg/db-sdk 共发布 2 个版本,首次发布于 2026/06/08;过去 12 个月发布了 2 个版本。

版本发布日期距今
0.2.0latest2026/09/241 个月前
0.1.02026/06/085 个月前

关于 @broberg/db-sdk 的常见问题

@broberg/db-sdk 是什么?

Thin shared database client for the broberg.ai fleet — one DbClient facade over libSQL/Turso and PostgreSQL/Supabase. The SQL dialect is passed through, not translated.

如何安装 @broberg/db-sdk?

在项目目录运行 npm install @broberg/db-sdk,或使用 pnpm add / yarn add / bun add。

@broberg/db-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@broberg/db-sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@broberg/db-sdk 可以商用吗?

@broberg/db-sdk 使用 FSL-1.1-Apache-2.0 许可。声明的许可为 FSL-1.1-Apache-2.0,请阅读许可原文确认使用条件。以上为概括,不构成法律意见,请以许可原文为准。

@broberg/db-sdk 的下载量有多少?

根据 npm 官方下载统计,@broberg/db-sdk 在 2026/10/09 – 2026/11/07 这 30 天内被下载 6,173 次,日均约 206 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@broberg/db-sdk 还在维护吗?

最新版本 0.2.0 发布于 2026/09/24(1 个月前);至今共发布 2 个版本,其中过去 12 个月发布了 2 个;首次发布于 2026/06/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。