Skip to content

Database

Zelavis App includes a project database service with documents, events, schemas, projections, and time-series. It is separate from the Platform OS System Store.

This page is the in-process API, for code running inside a Project. An App reaching its own records from outside — a desktop client, a worker, another host — uses App Data, which is the same database behind a boundary that resolves the Tenant from the caller.

Ordinary App data access always begins with a real Tenant ID. There is no implicit default Tenant:

import { Zelavis } from 'zelavis';
const zv = new Zelavis({ adapter });
const tenantDb = zv.db.forTenant('tenant_acme');
await tenantDb.documents.createCollection({ name: 'posts' });
const doc = await tenantDb.documents.insert({
collection: 'posts',
data: { title: 'Hello', published: false },
});
await tenantDb.documents.update({
collection: 'posts',
id: doc.id,
data: { published: true },
});

Tenant identity is not accepted inside document or event inputs. Binding it once prevents a caller from accidentally mixing data from different Tenants in one operation sequence.

The logical database API deliberately does not expose a physical driver, SQLite filename, or raw SQL connection.

Property Description
forTenant(tenantId) Returns Tenant-bound documents, events, and time-series APIs.
schemas Global collection schema definitions applied across shards.
projections Projection definitions and rebuild controls.
timeseries Global time-series definition registry.
capabilities Logical database capabilities such as Tenant routing.
context Runtime configuration and Node identity.

Low-level engine adapters still expose physical SQL on DatabaseDriver.sql for explicitly targeted administration and adapter tests. It is not part of DatabaseApi, because one logical App database can span several SQLite files.

tenantDb.documents.createCollection(input)
tenantDb.documents.listCollections()
tenantDb.documents.collectionExists(input)
tenantDb.documents.insert(input)
tenantDb.documents.findById(input)
tenantDb.documents.findMany(input)
tenantDb.documents.update(input)
tenantDb.documents.delete(input)

Documents are JSON objects stored in named per-collection tables. Writes append an event before updating the materialized collection table.

const page = await tenantDb.events.read({
collection: 'posts',
limit: 100,
});
const nextPage = await tenantDb.events.read({
collection: 'posts',
after: page.at(-1)?.cursor,
limit: 100,
});

DatabaseEvent.cursor is an opaque, versioned continuation token. Persist it and pass it back unchanged. Do not parse, compare, increment, or assume it is a global integer: the topology router binds cursors to the Tenant’s virtual shard.

Schema and projection definitions live on the logical database because they must be consistent across every physical shard:

await zv.db.schemas.save({
collection: 'posts',
version: 1,
activate: true,
fields: [
{
name: 'title',
field: { _tag: 'TextField', label: 'Title', required: true },
},
],
});
await zv.db.projections.register({
name: 'published-posts',
source: {
collections: ['posts'],
eventTypes: ['document.upserted'],
},
});

Definitions are logical and Tenant-neutral; reads are Tenant-bound:

await zv.db.timeseries.define({
name: 'post-writes',
source: {
collections: ['posts'],
eventTypes: ['document.upserted'],
},
map: (event) => ({
timestamp: event.timestamp,
value: 1,
}),
});
const handle = tenantDb.timeseries.get('post-writes');
const points = await handle.range({ order: 'desc', limit: 50 });
const total = await handle.aggregate({ op: 'count' });

The same definition can be queried for another Tenant through a different forTenant(...) handle without changing its mapping logic.