docsPackagessmartchats-database

smartchats-database

The data layer for SmartChats. Pure SurrealQL query builders, an SDK client, the local schema and a multi-step operations layer. The query builders do no I/O.

The principle: separate query construction from query execution. Builders return QuerySpec values (a query string and variables). The runtime hands those specs to a Client that knows how to talk to a real SurrealDB instance. This lets the same query code run in the browser (through the backend’s DataAPI), in Node (CLI and MCP, direct SDK), or in tests.

Layered model

your code  →  operations.X(data, opts)   multi-step (exportBundle, importBundle)
                       │
                       ▼
              queries.Y(args)            pure: args → { query, variables }
                       │
                       ▼
     data.query(spec)  or  client.runQuery(spec)    I/O happens here
            │                        │
     DataAPI (backend)        Client (createClient: direct
     cloud or local           SurrealDB connection)

What’s in the package

ModulePurpose
queries/*Pure QuerySpec builders, one file per domain (sessions, logs, knowledge_graph, todos, metrics, media, byo_keys and more). Exported at the top level and as the queries namespace
operations/*exportBundle and importBundle, plus DEFAULT_EXPORT_TABLES, SENSITIVE_TABLES, NEVER_EXPORT_TABLES
schema/*The local schema: LOCAL_DDL, LOCAL_SCHEMA_VERSION, LOCAL_SCHEMA_MIGRATIONS, applyLocalSchema. Exported as the schema namespace
client.tscreateClient, createLazyClient, createUserClient: direct SurrealDB connections
data_api.tsmakeDataAPI, makeCloudDataAPI, makeLocalDataAPI. Node only, imported from smartchats-database/data-api
id.tsstringifyRecordId, parseRecordIdArg
types.tsQuerySpec, AuditFields, EventTimeFields

Quick start

Building a query

import { queries } from 'smartchats-database';
 
const spec = queries.listSessions({ limit: 50 });
 
// spec is { query: "SELECT ... FROM sessions ...", variables: {...} }

The builder has zero side effects. You can log it, snapshot-test it, inspect what would run, without touching a database.

Running a query (Node, direct SurrealDB)

import { createClient, queries } from 'smartchats-database';
 
const client = createClient({
  url: 'ws://127.0.0.1:8000/rpc',
  namespace: 'smartchats',
  database: 'main',
  auth: { username: 'root', password: 'root' },
});
await client.connect();
 
const sessions = await client.runQuery(queries.listSessions({ limit: 10 }));

createLazyClient connects on the first query. createUserClient connects with a record-access token so per-user permissions apply. A Client also has runRaw(query, variables) for per-statement results, insert and query.

For a DataAPI without the app, Node code can use the factories on the data-api subpath:

import { makeDataAPI } from 'smartchats-database/data-api';
 
const { data, close } = await makeDataAPI('local');   // or 'cloud'

Running a query (browser, through DataAPI)

The browser app doesn’t talk to SurrealDB directly. It goes through the backend contract:

import { getBackend } from '@/lib/backend';
import { queries } from 'smartchats-database';
 
const backend = getBackend();
const { rows } = await backend.data.query(queries.listSessions({ limit: 10 }));

Same QuerySpec. The cloud backend sends it to a Cloud Function that runs it against SurrealDB. The local backend sends it to the local server’s /data/query route. The call site doesn’t change.

Operations

Some workflows need several queries + ordering + error handling. Those live in operations/:

import { operations } from 'smartchats-database';
 
const { bundle, perTable, blockedTables } = await operations.exportBundle(backend.data, {
  source: 'cloud',
  userId,
});
 
const result = await operations.importBundle(otherBackend.data, bundle);

Both take a DataAPI. exportBundle options include tables (default DEFAULT_EXPORT_TABLES), includeSensitive, pageSize and onProgress. importBundle options include tables and concurrency. A Bundle is { version: 1, exportedAt, source, userId, tables, ... }. This is how data moves between cloud and local.

Schema

The local schema lives in packages/smartchats-database/src/schema/local.ts. schema.applyLocalSchema(db, opts) applies it idempotently, and the local server calls it at startup. The cloud’s per-user schema is not part of this package.

SurrealDB v3. Notable v3 differences:

  • type::record replaces type::thing for record constructors
  • type::is_string(x) (not type::is::string(x)) for type predicates
  • The logs table is SCHEMALESS. content can be NONE, a string, or a record link, so builders that match on content guard with type::is_string(content)

Why no I/O at the query layer?

  1. Tests. Tests on QuerySpec outputs catch query changes without a database.
  2. Reuse. The same query runs in the browser (through DataAPI), in Node (direct SDK) and in tests.
  3. Cloud vs local symmetry. Both backends accept QuerySpec. Adding a new query means writing one builder, not two.

See also