@dvina/sdk
Type-safe SDK for the Dvina GraphQL API. Offline-first architecture with normalized local store (IndexedDB), real-time sync, a React adapter and an AI SDK chat transport.
Installation
npm i @dvina/sdk
Workspace Development
From the repository root:
pnpm bootstrap
pnpm --filter @dvina/sdk run generate:workspace
pnpm --filter @dvina/sdk run build
Or use the root shortcut:
pnpm build:sdk
generate:workspace first builds the codegen helper packages, then regenerates SDK sources. build:workspace runs the full generate + build flow in one command.
Peer dependency:
npm i graphql
Framework adapters (optional):
# React
npm i react
# Chat (AI SDK)
npm i ai
Quick Start
import { DvinaSdk } from '@dvina/sdk';
const sdk = new DvinaSdk({ token: 'your-auth-token' });
For production apps with token refresh:
const sdk = new DvinaSdk({
getToken: async () => {
// return a fresh token from your auth provider
return await authService.getAccessToken();
},
});
Options
| Option | Type | Description |
|---|---|---|
token | string | Static auth token (scripts, CLI, tests) |
getToken | () => Promise<string> | Async token callback (production apps) |
baseUrl | string | API base URL without protocol. Default: api.dvina.ai |
language | string | (() => string) | Preferred language for API responses |
Queries
Fetching a Single Entity
const chat = await sdk.chat({ id: 'chat-id' }).fetch();
console.log(chat.id);
console.log(chat.title);
Fetching a Connection (List)
const insights = await sdk.insights({ first: 20 }).fetch();
for (const insight of insights.nodes) {
console.log(insight.id, insight.name);
}
console.log(insights.totalCount);
console.log(insights.pageInfo.hasNextPage);
Smart Fetch (Including Relations)
Chain relation methods before .fetch() to include related data in a single request:
// Include agent data inline with each chat
const chats = await sdk.chats({ first: 50 }).agent().fetch();
// agent is directly available on each node (no extra request)
for (const chat of chats.nodes) {
console.log(chat.agent.id, chat.agent.name);
}
// Single entity with includes
const chat = await sdk.chat({ id: 'chat-id' }).agent().fetch();
console.log(chat.agent.name);
Nested includes with variables:
const databases = await sdk.databases({ first: 100 }).tables({ first: 10 }).fetch();
for (const database of databases.nodes) {
for (const table of database.tables.nodes) {
console.log(table.name);
}
}
Lazy Relation Fetching
Without Smart Fetch, relations are fetched on demand via method calls:
const chat = await sdk.chat({ id: 'chat-id' }).fetch();
// Separate request for the agent
const agent = await chat.agent().fetch();
console.log(agent.name);
Pagination
Connections follow the Relay cursor specification with built-in pagination helpers:
const insights = await sdk.insights({ first: 20 }).fetch();
// Load next page (appends nodes, updates pageInfo)
if (insights.pageInfo.hasNextPage) {
await insights.fetchNext();
}
// Load previous page (prepends nodes, updates pageInfo)
if (insights.pageInfo.hasPreviousPage) {
await insights.fetchPrevious();
}
Utility functions:
import { extractNodes, mergeConnections } from '@dvina/sdk/pagination';
const allNodes = extractNodes(insights);
// Returns a new connection without modifying the originals
const merged = mergeConnections(firstPage, secondPage);
Mutations
// Create
const agent = await sdk.createAgent({
data: { name: 'My Agent', description: 'An assistant' },
});
// Update
const updated = await sdk.updateInsight({
where: { id: 'insight-id' },
data: { name: 'Updated Name' },
});
// Delete
await sdk.deleteChat({ where: { id: 'chat-id' } });
Mutations automatically update the local store with optimistic updates and cache rules.
Reactive Queries (watch)
Use .watch() instead of .fetch() to get a reactive DvinaQueryRef that emits new values whenever the underlying data changes (via local mutations, delta sync, or cache updates):
const ref = sdk.insights({ first: 20 }).watch();
// Vanilla JavaScript — AsyncIterable
for await (const insights of ref) {
console.log('Updated:', insights.nodes.length);
}
// Clean up when done
ref.dispose();
DvinaQueryRef properties:
| Property/Method | Description |
|---|---|
current | Most recently emitted value (or undefined) |
refetch() | Force re-fetch from network |
fetchMore() | Fetch additional pages and merge |
dispose() | Unsubscribe and clean up |
Chat
Chat runs over the AI SDK UI message stream. @dvina/sdk/ai provides the transport for useChat and the message types:
import { useChat } from '@ai-sdk/react';
import { createDvinaChatTransport, stopDvinaChat, type DvinaUIMessage } from '@dvina/sdk/ai';
const chatOptions = { apiUrl: sdk.apiUrl, getToken: (options) => sdk.getToken(options) };
const transport = createDvinaChatTransport(chatOptions);
function Chat({ chatId }: { chatId: string }) {
const { messages, sendMessage } = useChat<DvinaUIMessage>({ id: chatId, transport, resume: true });
// render messages, call sendMessage({ text })
}
// Stop the running answer, on whichever API instance runs it
await stopDvinaChat(chatOptions, chatId);
The transport sends only the newest message; the server keeps the history. A reconnecting client resumes the running stream.
Realtime events
User-scoped updates arrive over the sync SSE connection as snapshots:
const usage = sdk.events.tokenUsageStatus().watch(); // also activeChats(), devicePresence(), mcpAuth()
for await (const event of usage) {
console.log(event.data);
}
mcpAuth() lists MCP servers waiting for the user to authorize (open authorizationUrl) and recent outcomes.
File Upload
const file = new File(['content'], 'document.pdf', { type: 'application/pdf' });
const result = await sdk.uploadFile(file, 'document.pdf', {
onProgress: (event) => {
console.log(`${event.percent}% uploaded`);
},
});
console.log('File ID:', result.id);
Framework Adapters
React
import { useLiveQuery } from '@dvina/sdk/react';
function InsightsPage() {
const insights = useLiveQuery(() => sdk.insights({ first: 20 }).watch(), []);
if (!insights) return <div>Loading...</div>;
return (
<ul>
{insights.nodes.map((i) => (
<li key={i.id}>{i.name}</li>
))}
</ul>
);
}
The hook is built on useSyncExternalStore. The query ref is created again only when the dependency array changes, so inline .watch() calls are safe.
// Reactive query — ref recreated when `id` changes
function InsightDetail({ id }: { id: string }) {
const insight = useLiveQuery(() => sdk.insight({ id }).watch(), [id]);
if (!insight) return <div>Loading...</div>;
return <h1>{insight.name}</h1>;
}
useLiveQuery disposes the query ref once no component uses it (unmount or dependency change); a component shown again gets a fresh ref.
Error Handling
The SDK provides a typed error hierarchy:
import { DvinaError, DvinaGraphQLError, DvinaNetworkError, DvinaAuthenticationError } from '@dvina/sdk';
try {
await sdk.chat({ id: 'invalid' }).fetch();
} catch (error) {
if (error instanceof DvinaAuthenticationError) {
// 401/403 — redirect to login
} else if (error instanceof DvinaGraphQLError) {
// Server returned GraphQL errors
console.error(error.errors);
} else if (error instanceof DvinaNetworkError) {
// Connection failure, timeout, etc.
console.error(error.status);
}
}
| Error Class | When |
|---|---|
DvinaError | Base class for all SDK errors |
DvinaGraphQLError | GraphQL response contains errors |
DvinaNetworkError | Network failure, timeout, non-OK HTTP |
DvinaAuthenticationError | 401/403 authentication failure |
Architecture
The SDK uses an offline-first, normalized store architecture:
- Queries are routed through the
SyncEngine, which normalizes GraphQL responses into entity tables in IndexedDB (via Dexie). - Mutations apply optimistic updates immediately, then reconcile with server responses.
- Delta Sync maintains a persistent SSE connection that streams entity changes, keeping the local store up to date in real-time.
- Reactive queries (
watch()) are backed by Dexie'sliveQuery— any write to a relevant table automatically re-emits.
DvinaSdk
├── Queries/Mutations → HTTP Transport → SyncEngine → IndexedDB (Dexie)
├── Delta Sync → SSE Transport → IndexedDB (Dexie)
├── Realtime events → SSE Transport → sdk.events
└── Chat → AI SDK UI message stream (@dvina/sdk/ai)
Cleanup
// Stop delta sync
sdk.stopSync();
// Full teardown (stops sync and the billing stream, closes the local database)
sdk.destroy();
License
MIT