Data Model
Objects, fields, relationships, validation, indexes — described to AI or written in TypeScript.
Data Model
The data model is the single source of truth for your app. Once an object exists, ObjectOS gives you REST APIs, a Console view, RBAC checkpoints, audit log entries, and AI tool exposure — for free.
Most customers never write the schema by hand. They describe what they need in the AI Builder and the platform creates the objects, fields, indexes, and translations. This page describes the underlying shape — so you understand what the AI is generating and can edit it directly when you want to.
Authoring paths
| Path | Looks like |
|---|---|
| AI Builder (primary) | "Create a support_ticket object with subject, description, priority, status, assignee." |
| Console click-build | Console → Objects → New Object → forms |
TypeScript (*.object.ts) | The TS shown below — typically inside a forked template |
All three produce the same schema. The schema is canonical; everything else is derived.
Anatomy of an object
// src/objects/task.ts
import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Task = ObjectSchema.create({
name: 'todo_task',
label: 'Task',
pluralLabel: 'Tasks',
icon: 'check-square',
description: 'A single unit of work.',
fields: {
subject: Field.text({ label: 'Subject', required: true, maxLength: 200 }),
description: Field.markdown({ label: 'Description' }),
status: Field.select({
label: 'Status',
options: [
{ label: 'To Do', value: 'todo', default: true },
{ label: 'In Progress', value: 'in_progress' },
{ label: 'Done', value: 'done' },
],
}),
due: Field.date({ label: 'Due' }),
assignee: Field.lookup({ label: 'Assignee', reference: 'sys_user' }),
},
enable: {
trackHistory: true, // record field changes in audit log
apiEnabled: true, // expose REST endpoints (default true)
feeds: true, // chatter / comments / @mentions
},
});Register it in your stack:
// objectstack.config.ts
import { defineStack } from '@objectstack/spec';
import * as objects from './src/objects';
export default defineStack({
manifest: { id: 'my.app', namespace: 'myapp', version: '0.1.0', type: 'app', name: 'My App' },
objects: Object.values(objects),
});That's all you need. os dev recompiles, and /api/v1/data/todo_task,
the Console Task view, and the Console permission row all appear.
Field types
ObjectStack ships ~25 field types. The most-used ones:
Scalars
| Type | What it stores | Helper |
|---|---|---|
text | Short string | Field.text({ maxLength, required }) |
textarea | Long string | Field.textarea(...) |
markdown | Rich text with markdown | Field.markdown(...) |
number | Integer | Field.number({ min, max }) |
decimal | Exact decimal (money, etc.) | Field.decimal({ precision, scale }) |
boolean | True/false | Field.boolean({ defaultValue }) |
date | Calendar date | Field.date(...) |
datetime | Timestamp | Field.datetime(...) |
email | Validated email | Field.email(...) |
url | Validated URL | Field.url(...) |
phone | Validated phone | Field.phone(...) |
json | Arbitrary JSON | Field.json(...) |
Choices
| Type | Use for |
|---|---|
select | Single choice (enum) |
multiselect | Multiple choices |
Relationships
| Type | Cardinality | Helper |
|---|---|---|
lookup | One-to-many (FK) | Field.lookup({ reference: 'sys_user' }) |
masterDetail | One-to-many with cascade delete | Field.masterDetail({ reference: 'order' }) |
Files & media
| Type | What it stores |
|---|---|
attachment | One file via the storage service |
image | Image attachment with preview |
Computed / derived
| Type | Behavior |
|---|---|
formula | Computed at read time from a CEL expression |
rollup | Aggregate of related records (sum/count/avg) |
autoNumber | Sequence (INV-{000001}) |
created, lastModified | System-maintained timestamps |
createdBy, lastModifiedBy | System-maintained user refs |
Required / unique / default
Common modifiers on every scalar field:
Field.text({
label: 'Code',
required: true,
unique: true, // unique constraint enforced at DB level
defaultValue: '',
helpText: 'Internal short code',
})Validation
Inline:
Field.number({ label: 'Quantity', min: 1, max: 9999 })
Field.text({ label: 'SKU', pattern: '^[A-Z]{3}-[0-9]{4}$' })Object-level rules (cross-field):
ObjectSchema.create({
name: 'order',
fields: { /* ... */ },
validations: [
{
name: 'discount_lt_total',
message: 'Discount cannot exceed total',
condition: 'discount < total',
},
],
});Validation runs on every write — REST, Console, ObjectQL — so there's no "back door."
Indexes & performance
ObjectSchema.create({
name: 'order',
fields: { /* ... */ },
indexes: [
{ fields: ['status', 'created_at'] },
{ fields: ['account', 'created_at'], unique: false },
],
});The driver creates real DB indexes on schema sync.
Field groups
For long forms, group fields in Console:
ObjectSchema.create({
name: 'task',
fieldGroups: [
{ key: 'core', label: 'Task', icon: 'check-square' },
{ key: 'planning', label: 'Planning', icon: 'calendar' },
{ key: 'meta', label: 'Metadata', icon: 'info', defaultExpanded: false },
],
fields: {
subject: Field.text({ label: 'Subject', group: 'core' }),
due: Field.date({ label: 'Due', group: 'planning' }),
},
});Lifecycle & ownership
ObjectSchema.create({
name: 'task',
ownership: 'own', // 'own' | 'shared' | 'system'
enable: {
apiEnabled: true, // generated REST endpoints
trackHistory: true, // audit log of field changes
feeds: true, // sys_comment / sys_activity / @mentions
softDelete: true, // tombstone instead of hard delete
},
});System objects (free with every project)
You don't have to declare these — they're always there:
| Object | What |
|---|---|
sys_user | User accounts |
sys_org | Organizations / tenants |
sys_member | Org membership |
sys_role, sys_permission_set | RBAC primitives |
sys_audit_log | Audit trail (when audit capability loaded) |
sys_file, sys_attachment | File metadata (when storage loaded) |
sys_comment, sys_activity | Feed / chatter (when feed loaded) |
sys_session, sys_api_key | Auth artifacts |
sys_webhook, sys_webhook_delivery | Webhook subs (when enabled) |
Reference them in lookup fields by name — e.g. Field.lookup({ reference: 'sys_user' }).
Polymorphic platform features
When you enable feeds: true and trackHistory: true, your object
automatically participates in:
sys_comment(thread_id =<object>:<id>)sys_attachment(parent_object =<object>, parent_id =<id>)sys_activity(timeline)sys_audit_log(field-level diffs)
You don't wire these per object — they're polymorphic on the platform.
Where to go next
- Permissions — gate access to your objects
- Flows / Automation — react to record changes
- API Access — call your generated REST
os explain— print the rendered schema@objectstack/specsource — the schema is the contract; everything here is derived from it