You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Define business entities with ObjectSchema — the core building block of every ObjectStack application
Object Metadata
An Object is the foundational metadata type in ObjectStack. It defines a business entity — its fields, capabilities, indexes, and behaviors. Each Object maps to a database table/collection and automatically gets CRUD APIs, UI forms, and query support.
Machine name (snake_case). Immutable after creation.
label
string
optional
Human-readable singular label (e.g. 'Account')
pluralLabel
string
optional
Plural label (e.g. 'Accounts')
description
string
optional
Developer documentation
icon
string
optional
Icon name (Lucide/Material)
Data
Property
Type
Required
Description
fields
Record<string, Field>
✅
Field definitions. Keys must be snake_case.
indexes
Index[]
optional
Database performance indexes
datasource
string
optional
Target datasource ID. Default: 'default'
Display
Property
Type
Required
Description
nameField
string
optional
The stored field used as the record display name, e.g. 'name' or 'title' (ADR-0079). The deprecated alias displayNameField is still accepted.
titleFormat
string
optional
Deprecated (ADR-0079 → nameField). Render-only title template (e.g. '{{record.name}} - {{record.code}}'); an explicit nameField takes precedence
highlightFields
string[]
optional
Most-important fields in priority order — default list columns, cards, previews, detail highlight strip (ADR-0085; formerly compactLayout — the old spelling was retired and is now rejected)
stageField
string | false
optional
Linear lifecycle field; false declares the status field non-linear and suppresses stage heuristics (ADR-0085)
Capabilities (enable)
Control which platform features are active for this object:
enable: {trackHistory: true,// Field history tracking for auditsearchable: true,// Include in global search indexapiEnabled: true,// Expose via REST/GraphQL APIsapiMethods: ['get','list','create','update','delete'],files: true,// File attachmentsfeeds: true,// Activity feed and commentsactivities: true,// Tasks and events trackingtrash: true,// Soft delete with restoremru: true,// Most Recently Used trackingclone: true,// Deep record cloning}
Flag
Default
Description
trackHistory
false
Field history tracking for audit compliance
searchable
true
Index records for global search
apiEnabled
true
Expose object via automatic APIs
apiMethods
all
Whitelist of allowed API operations
files
false
Enable file attachments
feeds
true
Enable social feed and comments
activities
true
Enable tasks and events tracking
trash
true
Enable soft delete with restore
mru
true
Track Most Recently Used list
clone
true
Allow record deep cloning
Enterprise Features
Multi-Tenancy
Row-level tenant isolation in a shared database — the tenant field is injected
on write and enforced on read. Database-per-tenant isolation is an
environment/deployment choice (each environment carries its own database URL),
not object metadata.
Declares how long the object's data lives and how its space is reclaimed
(ADR-0057). The platform LifecycleService (registered by
ObjectQLPlugin, default-on) sweeps declared policies hourly. Objects
without a lifecycle block keep permanent record semantics — nothing is
ever deleted.
// High-frequency telemetry: rotation window + age reap
lifecycle: {class: 'telemetry',retention: {maxAge: '14d'},storage: {strategy: 'rotation',shards: 14,unit: 'day'},}// Ephemeral rows: TTL on the natural expiry fieldlifecycle: {class: 'transient',ttl: {field: 'expires_at',expireAfter: '1d'},}// Compliance ledger: hot window, then cold storage
lifecycle: {class: 'audit',retention: {maxAge: '90d'},archive: {after: '90d',to: 'archive',keep: '7y'},}// Mixed table (live workflow state + terminal history): scope the reaplifecycle: {class: 'telemetry',retention: {maxAge: '30d',onlyWhen: {status: {$in: ['completed','failed']}}},}
Key
Description
class
Persistence contract (see table below); record is the implicit default
retention.maxAge
Age-based reap on created_at
retention.onlyWhen
Row filter the reap is scoped to (per-field equality or { $in: [...] }); rows outside it are retained regardless of age. Incompatible with rotation storage and archive
ttl
Per-row expiry: field + expireAfter
storage
{ strategy: 'rotation', shards, unit } — time-shard + O(1) shard DROP (SQLite)
archive
Cold-store hand-off: after (must equal retention.maxAge) + to (datasource name) + optional keep
reclaim
Space reclamation after sweeps (default on for non-record)
Class
Contract
Typical use
record
Business truth — permanent, no policies allowed
accounts, invoices
audit
Compliance ledger — retain → archive → delete
audit logs
telemetry
High-frequency log — rotation / short retention
activity streams, job runs
transient
Ephemeral state — TTL auto-expire
receipts, device codes
event
Bus messages — very short TTL (hours)
scheduled fan-out
Enforcement rules:
A non-record class must declare at least one bounding policy
(retention, ttl, or rotation storage) — rejected at parse time
otherwise. Policies on record are rejected too.
Duration literals are <n> + h/d/w/y (e.g. '6h', '14d', '7y');
archive.after must equal retention.maxAge.
An archive-declared object is never hot-deleted before the archive
copy succeeded. No datasource registered under the archive.to name ⇒
rows are retained (safe default), not dropped.
Registering a datasource named telemetry routes every
telemetry/event/audit object to it — separate storage, opt-in purely
by the datasource's existence.
Operations knobs live in the lifecycle settings namespace: a runtime
enabled switch, tenant-scoped retention_overrides (a regulated tenant
sets years while dev keeps days), row quotas and growth_alert_rows
(observe-and-alert only). OS_LIFECYCLE_DISABLED=1 disables sweeping
entirely.
System object, protected from deletion (default: false)
managedBy
enum
Lifecycle bucket that sets the default CRUD affordances and write policy — 'platform' (default), 'config', 'system', 'engine-owned', 'append-only', 'better-auth'. See Lifecycle bucket below.
userActions
object
Per-object override of the CRUD affordances the managedBy default implies — { create?, edit?, delete?, import?, exportCsv? }. This is what makes a system/append-only object admin/user-writable. See Lifecycle bucket.
sharingModel
enum
Org-Wide Default record visibility (ADR-0055/0056/0090). Canonical four only: 'private', 'public_read', 'public_read_write', 'controlled_by_parent' (detail visibility derived from its master). The legacy aliases ('read', 'read_write', 'full') were removed from the enum (ADR-0090 D4) — authoring rejects them. Unset on a custom object resolves to 'private' (ADR-0090 D1)
ownership
enum
Record-ownership model: 'user' (default — injects the reassignable owner_id lookup, engaging owner-scoped RLS, "My" views and owner reports), 'org', or 'none' (no per-record owner — Dataverse-style catalog / junction tables, skips owner_id). Distinct from the package own/extend contribution kind.
managedBy declares which lifecycle bucket an object belongs to. It sets the
default CRUD affordances the UI renders and the write policy the platform
enforces. The enforced policy is the resolved affordance — the bucket default
with any userActions override applied (resolveCrudAffordances) — not the
bare bucket string.
Bucket
Default write policy
platform
Default. User-owned business data — full New / Import / Edit / Delete.
config
Admin-authored configuration — New / Edit / Delete, no CSV import.
system
Platform-defined schema holding admin/user-writable data (RBAC link tables, preferences, messaging config). Locked by default; each object opens its writes via userActions.
engine-owned
Runtime rows a platform service owns end to end — generic CRUD hidden, exposed ['get', 'list'] only, no user writes ever.
append-only
Immutable audit trail — View + Export only.
better-auth
Identity tables owned by the better-auth driver — generic user-context CRUD is suppressed; mutations flow through the auth API (sign-in, invite, reset).
engine-owned vs. writable system objects (ADR-0103). Two buckets share
the same locked default matrix but say different things:
engine-owned — jobs, notifications, approval runtime rows,
sys_record_share, sys_automation_run, the metadata store, sys_secret,
audit trails — written only by their owning service under a system context,
never through the generic /data API. A fail-closed guard
(assertEngineOwnedWriteAllowed) rejects user-context generic writes to them.
system — platform-defined schema holding admin/user-writable data: the
RBAC link tables, sys_user_preference, sys_approval_delegation, the
messaging config grids. These declare userActions to open the writes they
legitimately take:
exportconstSysUserPreference=ObjectSchema.create({name: 'sys_user_preference',managedBy: 'system',// Affordance only — RLS / delegated administration is the actual authz.userActions: {create: true,edit: true,delete: true},// …});
The same override works on append-only. userActions is an affordance
declaration; the real authorization for these rows is still enforced by RLS,
delegated administration, and permission sets.
A managed object may not advertise `enable.apiMethods` verbs its resolved
affordances forbid — the registry strips the contradiction at registration
(`reconcileManagedApiMethods`, ADR-0049). To expose a generic write verb on a
`system`/`append-only` object, declare the matching `userActions` rather than
listing the verb in `apiMethods`.