Collections
Collections
Everything in a workspace addresses collections. A model (src/data/model/+<m>.model.ts) is storage: fields, kinds, constraints and state. Its collection (src/data/collection/<m>/+collection.ts) is the exposed portion: which fields are read, which inputs are written, its one transform, its queries and its actions. Policies, reads, live views, search, agents, automations and seeds name collections, never models, and a model with no collection is reachable only through a relationship that an exposed collection exposes.
Models
// src/data/model/+bookings.model.ts (templates/on-demand-services)
import { model } from '@norbital-ai/bolt';
import { AREAS, REPEATS } from '../../lib/matching.ts';
/**
* One customer order: a service at an address, once or recurring, with or without preferred helpers. Each occurrence is
* a visit; the booking holds what they share.
*/
export default model({
description:
'A booking: customer, service, address, preference and repeat. Its visits are the occurrences.',
icon: 'lucide:calendar-check',
label: 'number',
fields: {
number: { kind: 'seq', pattern: 'BK-{yyyy}-{0000}' },
address: { kind: 'text' },
location: { kind: 'point', optional: true },
area: { kind: 'enum', values: AREAS, optional: true },
preference: { kind: 'enum', values: ['any', 'preferred'] },
repeat: { kind: 'enum', values: REPEATS },
status: {
kind: 'state',
initial: 'active',
states: { active: { to: ['cancelled'] }, cancelled: { edit: 'none' } }
},
notes: { kind: 'text', optional: true },
visit_count: { kind: 'count', of: 'visits' }
},
index: ['status']
}); The model literal takes description, icon, label (the record title) and fields, and the constraints key (the natural key used by upserts, imports and integrations), unique, index, check, noOverlap, computed and search. A model never names another model: references are relationships. History is always on.
Field kinds
Every field is a typed literal. optional: true makes it nullable; every other field is required. One language types stored fields and every input.
text(formatemail,phone,urlorzone;max;many),int,decimal(scale),money,currencyandbooldate,instant,time,duration, andperiod(of: 'date'orof: 'instant')enum(values,many),state(initial, and per statetoandedit), andseq, a number series such as'QT-{yyyy}-{0000}'that is never reusedsumandcountroll-ups over a relationship (of: 'quote_lines.line_total'), kept by the kernel inside the write statementjson(with a typedshape),file(accept,maxup to 20 MiB,multiple),point,vector(dimandmetric, always indexed) andcustom(a workspace custom field)- Inputs — of queries, actions, automations and integration records — add
number,id,list,object,unionandrecord.
State and locking
A state field is the only lock. to lists the edges out of a state, and edit lists what may change while a row is in it ('all' by default, 'none', or named fields and owned relationships). Both hold on every write path, administrators and automations included. Approval is never a state: it is a hold on the write, declared on the grant.
Relationships
// src/data/+relationship.ts (templates/on-demand-services, excerpt)
import { relationship } from '@norbital-ai/bolt';
export default relationship({
'helpers.user': { to: 'sys_user', optional: true, onDelete: 'setNull' },
'bookings.customer': { to: 'customers', inverse: 'bookings' },
'bookings.service': { to: 'services', inverse: 'bookings' },
'booking_helpers.booking': { to: 'bookings', inverse: 'preferred_helpers', owned: true },
'booking_helpers.helper': { to: 'helpers', inverse: 'preferred_by' },
'visits.booking': { to: 'bookings', inverse: 'visits', owned: true },
'visits.helper': { to: 'helpers', inverse: 'visits', optional: true, onDelete: 'setNull' },
'booking_requests.service': { to: 'services', where: { active: { eq: true } } }
// …
}); Every reference lives in src/data/+relationship.ts, keyed '<model>.<field>': the key names the foreign-key field the relationship adds to the model. to is a model (an array is an exclusive arc), inverse names the other side for roll-ups, filters and views, and onDelete is restrict (the default), cascade or setNull.
owned: true makes the child part of its parent: deleted with it, never moved to another parent, and written only while the parent’s state allows edits — a booking’s visits follow it this way. A reference to a person names the identity collection sys_user.
The collection
// src/data/collection/customers/+collection.ts (templates/on-demand-services)
import { collection } from '@norbital-ai/bolt';
const columns = ['name', 'phone', 'email', 'address', 'location', 'area', 'notes'] as const;
export default collection('customers', {
read: { fields: 'all', relations: 'all' },
create: { input: { columns } },
update: { input: { columns } },
delete: {}
}); read.fields bounds every surface: a grant, a view, the agent and an automation see nothing outside it, and read.relations names the relations a filter or a view may cross. create and update are input allowlists (columns, and relation actions under with); delete: {} exposes deletes. A collection with only read is read-only.
A collection’s transform, queries, actions and notifications are its write contract. Its optional +pipeline.ts sits beside it (Pipelines); integration definitions live in src/integration/<name>/+integration.ts (Integrations).
Search and similarity
search.text on the model turns on the search box of every view; search.semantic keeps an embedding over the named text and file fields. A vector field the collection fills itself gets named similarity searches: the literal declares the typed input, and c.similarity attaches the probe and an optional exact re-rank.
// src/data/model/+photo_evidence.model.ts (templates/field-operations, excerpt)
perceptual_embedding: { kind: 'vector', dim: 256, metric: 'l2', hidden: true },
scene_embedding: { kind: 'vector', dim: 256, metric: 'cosine', optional: true, hidden: true },
// src/data/collection/photo_evidence/+collection.ts (excerpt)
const photo_evidence = collection('photo_evidence', {
read: { fields: 'all' },
// …
similarity: {
pdq: {
description: 'Photos nearest a PDQ vector: near-duplicates of the same pixels.',
input: { vector: { kind: 'list', of: { kind: 'number' } } }
}
}
});
export default photo_evidence;
photo_evidence.similarity('pdq', {
probe: (input) => ({ field: 'perceptual_embedding', vector: input.vector })
}); Custom fields
A custom field is a stored kind of the workspace’s own. src/data/custom_field/<f>/+definition.ts declares its shape in the input language and may attach one validate; +renderer.svelte beside it displays and edits it. A model uses it as { kind: 'custom', of: '<f>' }.
// src/data/custom_field/photo_source/+definition.ts (templates/field-operations)
import { customField } from '@norbital-ai/bolt';
const photo_source = customField({
description:
'Where a photo came from: either a workspace upload, or the messaging conversation, message, attachment and sender that delivered it.',
shape: {
kind: 'union',
by: 'kind',
arms: {
workspace_upload: {},
message: {
integration: { kind: 'text' },
conversation_id: { kind: 'text' },
message_id: { kind: 'text' },
attachment_id: { kind: 'text' },
sender_id: { kind: 'text' },
sent_at: { kind: 'instant', optional: true }
}
}
}
});
export default photo_source;
photo_source.validate((value) =>
value.kind === 'message' &&
[value.integration, value.conversation_id, value.message_id, value.attachment_id, value.sender_id].some(
(part) => part.trim() === ''
)
? 'A message photo source names its integration, conversation, message, attachment and sender.'
: undefined
);
// used in src/data/model/+photo_evidence.model.ts as: source: { kind: 'custom', of: 'photo_source' } System collections
Identity and runs live in system collections the engine owns, such as sys_user, sys_team and sys_run. A relationship may name them (to: 'sys_user') and a policy grants reads on them; their writes are generated actions, never authored.