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:
| Column | Stored as | Notes |
|---|---|---|
text() | text | Base string field |
numeric() | numeric | Read 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 }) | timestamptz | An absolute instant at full precision ( UTC ISO ); `precision` only narrows the picker |
custom('instant_range', { multiple, precision }) | jsonb | A contiguous span of instants ( { start, end } ); `end` may be null (open span). Use multiple: true |
custom('money', { allowedCurrencies }) | jsonb | A monetary amount with its ISO 4217 currency code — a platform-owned datatype; redeclaring it is a compile error |
vector({ dimensions }) | vector | An embedding vector; dimensions are pinned at declaration time |
geolocation() | jsonb | GeoJSON-style point — { geometry: { lon, lat }, formatted_address, ... } . |
phone() | text | Telephone number with phone-specific editing semantics |
enums([...]) | text | Closed value set; values are validated at the boundary. Use .array() |
file({ mimeTypes, multiple }) | jsonb | Served 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 type | Named 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.
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>.tsmodule 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_sessionandchat_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.