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
| Module | Purpose |
|---|---|
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.ts | createClient, createLazyClient, createUserClient: direct SurrealDB connections |
data_api.ts | makeDataAPI, makeCloudDataAPI, makeLocalDataAPI. Node only, imported from smartchats-database/data-api |
id.ts | stringifyRecordId, parseRecordIdArg |
types.ts | QuerySpec, 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::recordreplacestype::thingfor record constructorstype::is_string(x)(nottype::is::string(x)) for type predicates- The
logstable is SCHEMALESS.contentcan be NONE, a string, or a record link, so builders that match oncontentguard withtype::is_string(content)
Why no I/O at the query layer?
- Tests. Tests on
QuerySpecoutputs catch query changes without a database. - Reuse. The same query runs in the browser (through
DataAPI), in Node (direct SDK) and in tests. - Cloud vs local symmetry. Both backends accept
QuerySpec. Adding a new query means writing one builder, not two.
See also
- smartchats-backend for
DataAPI - smartchats-sessions
- Source:
packages/smartchats-database/src/index.ts