Skip to content

Collections

Collections

A collection is one domain table plus its server behavior. Each collection is one directory at src/collections/<lower_snake_case_id>/ . The directory name is the collection ID; the model does not repeat it.

The model

import { defineModel, enums, text } from '@norbital-ai/bolt/authoring';

export default defineModel(
  {
    name: text().notNull(),
    status: enums(['active', 'complete'])
  },
  { description: 'Project site', recordLabel: 'name', icon: 'lucide:map-pin' }
);

A model holds storage and data identity only: the columns, plus description , recordLabel , icon , and indexes . Applications own presentation, so enum colors, default sorting, and renderer variants have no place in a model. Use enums([...]) for a closed value set, and point recordLabel at the column — or the array of columns — that names a record on screen. Save the declaration as src/collections/sites/+model.ts .

Column types

A field is a column. Bolt re-exports the base builders ( text , integer , boolean , uuid ) and adds domain column types with typed storage and matching UI behavior:

ColumnStored asNotes
text()textBase string field
numeric()numericRead as a JS number. numeric() takes no options — how a number is displayed belongs to the application, not to the column. Use integer for whole numbers, or text when a value only looks numeric, such as a reference code.
instant({ precision })timestamptzAn absolute instant at full precision ( UTC ISO ); `precision` only narrows the picker
custom('instant_range', { multiple, precision })jsonbA contiguous span of instants ( { start, end } ); `end` may be null (open span). Use multiple: true
custom('money', { allowedCurrencies })jsonbA monetary amount with its ISO 4217 currency code — a platform-owned datatype; redeclaring it is a compile error
vector({ dimensions })vectorAn embedding vector; dimensions are pinned at declaration time
geolocation()jsonbGeoJSON-style point — { geometry: { lon, lat }, formatted_address, ... } .
phone()textTelephone number with phone-specific editing semantics
enums([...])textClosed value set; values are validated at the boundary. Use .array()
file({ mimeTypes, multiple })jsonbServed entirely inline as a FileRef — its storage_key, file_name, file_size, and mime_type ride the row. Optional mimeTypes filter; use multiple: true
custom(kind)per custom typeNamed custom values defined in src/datatypes/<name>/ — see the UI components

Columns support the standard modifiers: .notNull() , .default(...) , .array() , and sql templates for generated defaults. Every row also carries the platform columns id , created_at , updated_at , and row_version automatically — you never declare them.

One model that puts several of them together:

import {
  custom, enums, file, geolocation, instant,
  numeric, phone, text, vector
} from '@norbital-ai/bolt/authoring';

export default defineModel(
  {
    title: text().notNull(),
    status: enums(['active', 'complete']).notNull().default('active'),
    progress: numeric(),
    starts_on: instant({ precision: 'day' }),
    window: custom('instant_range'),
    budget: custom('money'),
    embedding: vector({ dimensions: 1536 }),
    location: geolocation(),
    contact: phone(),
    report: file({ mimeTypes: ['application/pdf'] })
  },
  { description: 'Project site', recordLabel: 'title' }
);

Relationships

The single src/collections/+relationship.ts role defines relationships for the full registry and uses its adjacent generated type:

import type { Relationships } from './$types.js';

export default ((r) => ({
  sites: { site_visits: r.many.site_visits() },
  site_visits: {
    site: r.one.sites({ from: r.site_visits.site_id, to: r.sites.id })
  }
})) satisfies Relationships;

Companion roles

Server behavior lives beside the model in recognized role files. Each role default-exports one declaration and uses the adjacent generated ./$types.js ; no registration file is required.

  • +hooks.ts — per-record validation and same-transaction side effects ( Hooks )
  • +pipelines.ts — canonical collection import/export behavior ( Pipelines )
  • +integrations.ts — external receive/send bindings that reuse pipelines ( Integrations )
  • +representation.svelte — schema-derived form overrides ( UI components )

System collections

Every workspace ships with a fixed set of system collections defined in @norbital-ai/bolt . They are merged into your manifest at build time and power identity, access control, approvals, and files. You do not redefine them in tenant collections/ — you query them like any other collection.

Fixed schemas — query, do not redefine
The runtime merges these schemas in through withSystemCollections . Do not copy or override collections like user or approval_request in a tenant +model.ts — the platform depends on their exact shape. You can read and query them from apps, hooks, automations, and remote functions like any other collection.

Identity & access

  • user — one row per person: name, email, the admin flag (normal or admin), and the team they belong to. Who a team may DO is src/access/+teams.ts — a `src/access/+teams.ts` map compiled into the release, never a row.
  • session, account, verification, auth_config — sign-in sessions, linked credentials, verification tokens, and the secret that signs sessions. The runtime is their only writer.
  • Policies are not rows. A policy is a src/access/policies/+<name>.ts module in workspace source, compiled into the manifest alongside the collections it grants.

These are read-only from a workspace: the runtime’s system-collections policy grants read to every authenticated subject, and never write, so the runtime that owns a table stays its only writer. user is narrowed further — a query sees only id and the name, never the address. See Policies for how grants work on domain collections.

Review

  • approval_request — one open or closed approval flow over a collection mutation: which record it holds, its steps, and its status. See Approval workflows.
  • requestor — links an approval request to the user who raised it.

Files

  • file() — there is no platform file table. A `file()` column stores metadata inline ( FileRef { storage_key, file_name, file_size, mime_type }) and the host’s files facility resolves the bytes by `storage_key`. The column is a field of its record, so row predicates and field masks apply to it like anything else.

Runtime tables that are not collections

The runtime also provisions tables that are not collections. They are created by the schema plan rather than declared in the collection registry, so they carry no platform columns, never reach the browser replica, and cannot be read through the client:

  • bolt_collection_history — one row per create, update, or delete on a collection that keeps history, which every collection does unless its model turns it off: the operation, the subject who made it, and the snapshot.
  • bolt_audit — the append-only platform ledger, keyed by event kind and subject.
  • chat_session and chat_message — agent conversations and the turns recorded against them; these two are collections, so they are the one exception among the leaf tables.
  • bolt_notifications — in-app notification rows, written and read by the notifications facility.

vs domain collections

Domain collections are yours: payroll runs, shipments, work orders, and so on. System collections are the platform substrate every tenant shares — the runtime owns their shape, and a workspace reads them rather than writing them.

How collection data is read

In tenant apps, collection data is read through the live data layer: client.db.<collection>.findMany , findFirst , and count execute as live queries against a local replica. Hooks and remotes on the server still use api.db — live queries and optimistic mutations are the browser read and write path for operational UIs.