@kalamdb/client
v0.5.1-beta.2
Published
KalamDB client library for TypeScript, providing a WebSocket-based interface for interacting with KalamDB servers, supporting live queries, authentication, and real-time updates.
Downloads
208
Maintainers
Readme
@kalamdb/client
Official TypeScript / JavaScript SDK for KalamDB — SQL, realtime rows, and strong tenant isolation in one client.
Status: Beta — the API surface is still evolving.
KalamDB is built for apps where every user or tenant owns a private data space. The same SQL can run for every signed-in customer, while USER tables ensure each query only touches that caller's data. On the frontend, the default realtime API is now live(): you get the current materialized row set, not a stream of low-level diff frames that your UI has to reconcile.
→ kalamdb.org · Docs · GitHub
@kalamdb/client provides:
- SQL execution over HTTP
- materialized live query rows over WebSocket with
live()andliveTable() - low-level realtime events with
liveEvents()when you need raw frames - per-user and per-tenant isolation with USER tables
- FILE upload/download helpers
Runtime targets:
- Node.js
>= 18 - modern browsers
For React apps, install @kalamdb/react on top of this package. It wraps the same live() controller with KalamProvider, LiveQuery, LiveQueries, useLiveQuery, useLiveQueries, and assistant-friendly derived selection helpers.
Installation
npm i @kalamdb/clientThe browser build loads the packaged KalamDB WASM automatically. Most apps do not need to pass a custom wasmUrl.
Why live() First
Most UIs do not want subscription_ack, initial_data_batch, change, and error frames. They want the latest rows.
live() gives you exactly that:
- the current row set already reconciled for insert, update, and delete
- one callback shape for initial load and future changes
- shared behavior with the Rust and Dart clients
- simpler React, Vue, Svelte, and plain browser code
Use lastRows when you want an initial rewind from the
server. Use limit when you want the client to keep the materialized live row
set bounded over time.
The knobs apply at different layers:
batchSizechunks the initial snapshot from the serverlastRowschooses how much history to rewind firstlimitcaps the materialized live row set the client keeps afterward
Use liveEvents() only when you need the raw event protocol.
React users can delegate this row materialization to @kalamdb/react:
import { LiveQuery } from '@kalamdb/react';
<LiveQuery query="SELECT * FROM support.inbox WHERE room = 'main' ORDER BY created_at ASC">
{({ rows, state }) => (
<section>{state.loading ? 'Loading' : rows.length}</section>
)}
</LiveQuery>If your query does not expose an id column, pass column names through
getKey so row reconciliation still stays inside the shared Rust core:
const stop = await client.live(
"SELECT room_id, message_id, body FROM support.messages WHERE room_id = 'main'",
(rows) => {
console.log(rows.length);
},
{
getKey: ['room_id', 'message_id'],
},
);Quick Start
Start with a USER table. The SQL stays simple, and KalamDB scopes the data per authenticated user.
CREATE NAMESPACE IF NOT EXISTS support;
CREATE TABLE support.inbox (
id BIGINT PRIMARY KEY DEFAULT SNOWFLAKE_ID(),
room TEXT NOT NULL DEFAULT 'main',
role TEXT NOT NULL,
body TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
) WITH (TYPE = 'USER');import { Auth, createClient } from '@kalamdb/client';
const client = createClient({
url: 'http://localhost:2900',
authProvider: async () => Auth.basic('alice', 'Secret123!'),
});
function renderInbox(rows) {
console.log(
rows.map((row) => ({
id: row.id.asString(),
role: row.role.asString(),
body: row.body.asString(),
})),
);
}
const inboxSql = `
SELECT id, room, role, body, created_at
FROM support.inbox
WHERE room = 'main'
`;
const stop = await client.live(
inboxSql,
(rows) => {
// `support.inbox` is a USER table.
// Every signed-in user can run the same SQL text, but KalamDB only returns
// that caller's rows. No app-side WHERE user_id = ? filter is required.
renderInbox(rows);
},
{
limit: 200,
lastRows: 200,
onError: (event) => {
console.error(event.code, event.message);
},
},
);
await client.query(
'INSERT INTO support.inbox (room, role, body) VALUES ($1, $2, $3)',
['main', 'user', 'Need help with billing'],
);
// Later:
await stop();
await client.disconnect();Resume From a Specific SeqId
When you want offline resume or a durable checkpoint, persist the last SeqId you applied and feed it back into from.
import { Auth, SeqId, createClient } from '@kalamdb/client';
const client = createClient({
url: 'http://localhost:2900',
authProvider: async () => Auth.jwt(await getFreshToken()),
});
function renderInbox(rows) {
console.log(rows.length);
}
const inboxSql = `
SELECT id, room, role, body, created_at
FROM support.inbox
WHERE room = 'main'
`;
// Start from a specific known sequence ID.
// Replace '42' with a previously persisted checkpoint string when resuming.
const startFrom = SeqId.from('42');
let latestCheckpoint;
const stop = await client.live(
inboxSql,
(rows) => {
renderInbox(rows);
},
{
limit: 200,
lastRows: 200,
...(startFrom ? { from: startFrom } : {}),
onCheckpoint: ({ lastSeqId }) => {
// Persist the last fully applied server sequence so the next session can
// continue from that exact point.
latestCheckpoint = lastSeqId.toString();
},
},
);
console.log('latest checkpoint', latestCheckpoint);This pattern is useful for chat threads, activity feeds, audit logs, and any UI that wants fast reconnect without replaying everything from zero.
Preserve Tenant Boundaries in Worker Writes
Background services should keep the same user isolation guarantees as the browser. Direct USER-table and STREAM-table access stays scoped to the authenticated account; executeAsUser() switches subject only when KalamDB authorizes the actor role for the target user's role.
await client.executeAsUser(
'INSERT INTO support.inbox (room, role, body) VALUES ($1, $2, $3)',
'alice',
['main', 'assistant', 'Your billing issue is being reviewed'],
);That keeps the write inside Alice's USER-table or STREAM-table partition through an explicit, audited delegation boundary instead of leaking service-side writes into the wrong tenant scope.
Lower-Level Realtime API
If you need raw protocol frames, use liveEvents().
import { ChangeType, MessageType } from '@kalamdb/client';
const stop = await client.liveEvents(
"SELECT * FROM support.inbox WHERE room = 'main'",
(event) => {
// Use this path when you need raw subscription protocol events.
if (event.type !== MessageType.Change) {
return;
}
if (event.change_type === ChangeType.Insert) {
console.log('new rows', event.rows);
}
},
{ batchSize: 200, lastRows: 200 },
);Use this API for protocol tooling, debugging, or custom reconciliation. For app UI state, prefer live().
Topics and Workers
Topic workers now live in the separate @kalamdb/consumer package so app-only installs keep the main SDK lean.
Install the worker package only when you need topic consumption or the worker runtime:
npm i @kalamdb/client @kalamdb/consumerUse @kalamdb/client for app-facing SQL, live rows, subscriptions, auth, and files. Use @kalamdb/consumer for consumeBatch(), ack(), consumer(), and runConsumer().
Files
queryWithFiles() sends multipart uploads directly to KalamDB while keeping the same auth flow as the rest of the SDK.
await client.queryWithFiles(
'INSERT INTO support.attachments (id, file_data) VALUES ($1, FILE("upload"))',
{ upload: selectedFile },
['att_1'],
(progress) => {
console.log(progress.file_name, progress.percent);
},
);Authentication
authProvider is the canonical way to configure the client.
import { Auth, createClient, type AuthProvider } from '@kalamdb/client';
const authProvider: AuthProvider = async () => {
const token = await myApp.getOrRefreshJwt();
return Auth.jwt(token);
};
const client = createClient({
url: 'http://localhost:2900',
authProvider,
});The SDK handles:
- WASM initialization
- Basic-auth-to-JWT exchange
- lazy or eager WebSocket connection
- reconnect controls and
SeqIdtracking
Tested Examples
The npm README examples are backed by SDK tests:
tests/readme-examples.test.mjscoverslive(), resume-from-SeqId,executeAsUser(), andqueryWithFiles().tests/single-socket-subscriptions.test.mjscovers shared-socket subscriptions and materialized live rows.
API Pointers
query(),queryOne(),queryAll(),queryRows()for SQL readsinsert(),update(),delete()for convenience DMLlive()andliveTable()for materialized realtime rowsliveEvents()for low-level subscription framesgetSubscriptions()for active subscriptions and typedlastSeqIdcheckpoints
Full docs: kalamdb.org/docs/sdk/typescript
Browser and Node.js support is powered by a Rust core compiled to WebAssembly (WASM). See DEV.md for build and contribution details.
License
Licensed under the Apache License, Version 2.0 (Apache-2.0). See the packaged LICENSE.txt and NOTICE files.
