Skip to main content
Version: 0.3.36

JavaScript/TypeScript Client

Build applications with @raisindb/client, the official SDK. It talks to the server over WebSocket for real-time work and over HTTP for server-side rendering, uploads and identity auth.

Installation

npm install @raisindb/client

In Node.js also install the optional peer dependency ws (npm install ws); browsers use the built-in WebSocket.

Quick start

import { RaisinClient } from '@raisindb/client';

const client = new RaisinClient('ws://localhost:8080', { repository: 'myapp' });

await client.connect();
await client.authenticate({ username: 'admin', password: 'your-password' });

const db = client.database('myapp');
const ws = db.workspace('content');

const page = await ws.nodes().create({
type: 'raisin:Page',
path: '/articles/hello',
properties: { title: 'Hello' },
});

const result = await db.executeSql(
"SELECT path, properties->>'title' AS title FROM 'content' WHERE node_type = $1",
['raisin:Page'],
);
console.log(result.rows); // [{ path: '/articles/hello', title: 'Hello' }]

The client connects to ws://host/ws/{repository}. You can pass that path yourself (new RaisinClient('ws://localhost:8080/ws/myapp')) or give a bare host plus the repository option. Multi-tenant operators can address a specific tenant with ws://host/sys/{tenant}/{repository}.

HTTP-only client (server-side rendering)

Where WebSocket is not available, or for one request per page load, use the HTTP client. It shares authentication and SQL with the WebSocket client but has a smaller node API (see below).

import { RaisinClient } from '@raisindb/client';

const http = RaisinClient.forSSR('http://localhost:8080');
await http.authenticate({ username: 'admin', password: process.env.RAISIN_PASSWORD! });

const db = http.database('myapp');
const { rows } = await db.executeSql("SELECT path, properties FROM 'content' WHERE path = $1", ['/articles/hello']);
const page = await db.workspace('content').getNodeByPath('/articles/hello');

Authentication

Admin credentials or a token

await client.authenticate({ username: 'admin', password: 'your-password' });
// or reuse a JWT / API key you already hold
await client.authenticate({ type: 'jwt', token: 'eyJhbGc...' });

End users (identity auth)

Register and log in application users with the built-in identity system. The repository is required because identities and roles are per repository.

const user = await client.registerWithEmail('alice@example.com', 'CorrectHorse1!', 'myapp', 'Alice');
const user = await client.loginWithEmail('alice@example.com', 'CorrectHorse1!', 'myapp');
console.log(user.email, user.home); // 'alice@example.com', '/users/internal/alice-at-example-com'

Magic links, OIDC and the lower-level endpoints are covered in Identity Authentication.

Restore a session

Tokens are kept in a TokenStorage. The default keeps them in memory; in a browser use LocalStorageTokenStorage so a reload can pick the session back up:

import { RaisinClient, LocalStorageTokenStorage } from '@raisindb/client';

const client = new RaisinClient('ws://localhost:8080', {
repository: 'myapp',
tokenStorage: new LocalStorageTokenStorage('myapp'),
});
await client.connect();

const user = await client.initSession('myapp'); // IdentityUser | null
if (!user) redirectToLogin();

Auth and readiness events

const off = client.onAuthStateChange(({ event, session }) => {
// event: 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'SESSION_EXPIRED' | 'USER_UPDATED'
console.log(event, session.user?.email);
});

client.onReadyStateChange((ready) => console.log('connected and authenticated:', ready));
console.log(client.isReady());

onAuthStateChange fires for identity sign-ins (loginWithEmail, initSession, refresh); authenticate() with admin credentials does not emit it.

Working with nodes

const ws = db.workspace('content');
const nodes = ws.nodes();

// create
const article = await nodes.create({
type: 'raisin:Page',
path: '/articles/hello-world',
properties: { title: 'Hello World', status: 'draft' },
});

// read
const byId = await nodes.get(article.id);
const kids = await nodes.listChildren('/articles'); // editorial order
const page = await nodes.listChildrenPage('/articles', { limit: 50 });

// update: properties are replaced, send the full set
await nodes.update(article.id, { properties: { title: 'Hello World', status: 'published' } });

// delete
await nodes.delete(article.id); // true
const hello = await nodes.getByPath('/articles/hello-world');   // Node | null
const pages = await nodes.queryByType('raisin:Page', 20);
const published = await nodes.queryByProperty('status', 'published');

For anything beyond a path, type or single-property lookup, use SQL from the client (see SQL).

Tree operations

await nodes.move('/articles/old', '/archive');           // move under a new parent
await nodes.rename('/articles/hello-world', 'hello'); // rename in place
await nodes.copy('/articles/template', '/articles', 'new-article');
await nodes.copyTree('/articles/series', '/archive'); // deep copy

Ordering

Children have a manual order. You name a position or a neighbour and the server assigns the order key; children are identified by name, not full path.

await nodes.reorder('/articles', 'item-1', 0);               // 0-based position
await nodes.moveChildBefore('/articles', 'item-2', 'item-1');
await nodes.moveChildAfter('/articles', 'item-3', 'item-2');

Read the order back with the __order SQL column. Order is per branch and travels with a merge; when promoting content by copying nodes between branches, replay it with applyChildOrder(parentPath, sourceBranch). See Node Operations.

Relationships

await nodes.addRelation('/articles/hello-world', 'authored_by', '/users/john-doe');
await nodes.addRelation('/articles/review', 'reviews', '/products/item-123', { weight: 0.9, targetWorkspace: 'products' });
const rels = await nodes.getRelationships('/articles/hello-world'); // { outgoing, incoming }
await nodes.removeRelation('/articles/hello-world', '/users/john-doe');

SQL queries

const result = await db.executeSql(
"SELECT id, path, properties->>'title' AS title FROM 'content' WHERE node_type = $1 LIMIT $2",
['raisin:Page', 10],
);
// result: { columns: ['id','path','title'], rows: [{ id, path, title }], row_count: 1 }

The tagged template turns interpolations into parameters:

const status = 'published';
const { rows } = await db.sql`
SELECT path, properties->>'title' AS title
FROM 'content'
WHERE node_type = 'raisin:Page' AND properties->>'status'::String = ${status}
`;

Rows are plain objects keyed by column name.

Branches and revisions

const featureWs = db.onBranch('feature-xyz').workspace('content');
await featureWs.nodes().create({ type: 'raisin:Page', path: '/articles/new', properties: { title: 'New' } });

const old = await db.workspace('content').atRevision('1788719623132-0').nodes().get(article.id);

db.branches() forks, compares and merges branches; see Branches.

Real-time events

const sub = await ws.events().subscribe(
{ path: '/articles/**', event_types: ['node:created', 'node:updated', 'node:deleted'], include_node: true },
(event) => console.log(event.event_type, event.payload.path, event.payload.node?.properties),
);

const byType = await ws.events().subscribeToNodeType('raisin:Page', (event) => { /* ... */ });

await sub.unsubscribe();

Path filters are globs: a plain path matches only that node, /* its direct children, /** the whole subtree. Subscriptions are restored automatically after a reconnect. See Events and Realtime Subscriptions & Inbox.

AI chat

db.conversations is a ConversationManager for conversational AI. Conversations are raisin:Conversation nodes with raisin:Message children. See Chat & Conversations.

const { response } = await db.conversations.chat('/agents/my-assistant', 'What is RaisinDB?');

const conversation = await db.conversations.create({ participant: '/agents/my-assistant' });
for await (const event of db.conversations.sendMessage(conversation.conversationPath, 'Tell me more')) {
if (event.type === 'text_chunk') process.stdout.write(event.text);
}

For UI state, ConversationStore (and the framework hooks built on it) handles streaming text, tool-call tracking and plan approval.

Flows

db.flow is a FlowClient that runs server-side workflows over HTTP and streams their events:

const result = await db.flow.runAndWait('/flows/process-order', { orderId: '12345' });

const { instance_id } = await db.flow.run('/flows/generate-report', { month: '2025-01' });
for await (const event of db.flow.streamEvents(instance_id)) {
if (event.type === 'flow_completed') console.log(event.output);
}
await db.flow.resume(instanceId, { approved: true });

See Flows.

File uploads

const upload = await ws.upload(file, '/images/photo.jpg', {
onProgress: (p) => console.log(`${Math.round(p.progress * 100)}%`),
});

const batch = await ws.uploadFiles(fileList, '/images', { concurrency: 3 });

const { url } = await ws.signAssetUrl('/images/photo.jpg', 'display', { expiresIn: 600 });

The path is the asset node's full path (the file becomes that node). See Uploads.

Framework integrations

The package ships subpath exports for React (@raisindb/client/react), Svelte 5 (@raisindb/client/svelte) and Vue 3 (@raisindb/client/vue). Each provides auth, connection, SQL, subscription, conversation and flow bindings over the same stores.

import React from 'react';
import { createRaisinReact } from '@raisindb/client/react';
import { client } from './lib/raisin';

const { RaisinProvider, useConversation } = createRaisinReact(React);

function Chat() {
const chat = useConversation({
database: client.database('myapp'),
createOptions: { participant: '/agents/my-assistant' },
});
return (
<div>
{chat.messages.map((m, i) => <p key={i} className={m.role}>{m.content}</p>)}
{chat.isStreaming && <p>{chat.streamingText}</p>}
<button onClick={() => chat.sendMessage('Hello!')}>Send</button>
</div>
);
}

See Framework Integrations for the full list of hooks and adapters.

Error handling

import { RaisinError, RaisinConnectionError, RaisinAuthError, RaisinTimeoutError, RaisinFlowError } from '@raisindb/client';

try {
await nodes.update(id, { properties });
} catch (error) {
if (error instanceof RaisinAuthError) console.error('auth:', error.code, error.status);
else if (error instanceof RaisinTimeoutError) console.error('timed out after', error.timeoutMs, 'ms');
else if (error instanceof RaisinConnectionError) console.error('connection:', error.code);
else if (error instanceof RaisinFlowError) console.error('flow:', error.code, error.instanceId);
else if (error instanceof RaisinError) console.error(error.code, error.message);
}

Configuration options

import { RaisinClient, LocalStorageTokenStorage, LogLevel } from '@raisindb/client';

const client = new RaisinClient('ws://localhost:8080', {
repository: 'myapp', // builds the /ws/myapp route
defaultBranch: 'main',
requestTimeout: 30000,
logLevel: LogLevel.Info, // Silent | Error | Warn | Info | Debug
tokenStorage: new LocalStorageTokenStorage('myapp'),
connection: { autoReconnect: true, heartbeatInterval: 30000 },
});

See the ClientOptions reference.

Connection state

client.onConnectionStateChange((state) => {
// 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'closed'
});
client.onReconnected(() => {
// fires after reconnect, re-auth and subscription restore
});

Next steps