diff --git a/content/docs/concepts/architecture.mdx b/content/docs/concepts/architecture.mdx
index 9d765b0e3b..914025574a 100644
--- a/content/docs/concepts/architecture.mdx
+++ b/content/docs/concepts/architecture.mdx
@@ -95,6 +95,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Customer = ObjectSchema.create({
name: 'customer',
+ sharingModel: 'private',
label: 'Customer',
icon: 'building',
@@ -401,6 +402,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Opportunity = ObjectSchema.create({
name: 'opportunity',
+ sharingModel: 'private',
label: 'Opportunity',
icon: 'target',
diff --git a/content/docs/concepts/metadata-driven.mdx b/content/docs/concepts/metadata-driven.mdx
index 74aa5cfb2e..3e313bd46c 100644
--- a/content/docs/concepts/metadata-driven.mdx
+++ b/content/docs/concepts/metadata-driven.mdx
@@ -79,6 +79,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const User = ObjectSchema.create({
name: 'user',
+ sharingModel: 'private',
label: 'User',
icon: 'user',
@@ -243,6 +244,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Task = ObjectSchema.create({
name: 'task',
+ sharingModel: 'private',
label: 'Task',
icon: 'check-square',
@@ -341,6 +343,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Account = ObjectSchema.create({
name: 'account',
+ sharingModel: 'private',
label: 'Account',
fields: {
name: Field.text({ required: true }),
@@ -387,6 +390,7 @@ Follow these strict naming conventions for consistency:
// ✅ Correct naming
export const TodoTask = ObjectSchema.create({
name: 'todo_task', // snake_case machine name
+ sharingModel: 'private', // OWD — required on custom objects
label: 'Todo Task',
fields: {
@@ -463,6 +467,7 @@ annual_revenue: Field.currency({
```typescript
export const Account = ObjectSchema.create({
name: 'account',
+ sharingModel: 'private',
label: 'Account',
fields: { /* ... */ },
@@ -470,7 +475,7 @@ export const Account = ObjectSchema.create({
enable: {
trackHistory: true, // Enable field history tracking
searchable: true, // Include in global search
- apiEnabled: true, // Expose via REST/GraphQL
+ apiEnabled: true, // Expose object via automatic APIs (REST)
files: true, // Enable file attachments
feeds: true, // Enable activity feed
activities: true, // Enable tasks and events
@@ -486,6 +491,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const ExampleObject = ObjectSchema.create({
name: 'example_object', // Required: snake_case
+ sharingModel: 'private', // Required on custom objects: the OWD baseline
label: 'Example Object', // Required: Human-readable
pluralLabel: 'Example Objects', // Optional
icon: 'box', // Optional: Lucide icon name
diff --git a/content/docs/data-modeling/drivers.mdx b/content/docs/data-modeling/drivers.mdx
index 93ceb39953..1d9153446f 100644
--- a/content/docs/data-modeling/drivers.mdx
+++ b/content/docs/data-modeling/drivers.mdx
@@ -905,6 +905,7 @@ Then in your object definition:
```typescript
export const AuditLog = ObjectSchema.create({
name: 'audit_log',
+ sharingModel: 'private',
datasource: 'analytics', // Routes to the analytics database
fields: { /* ... */ },
});
diff --git a/content/docs/data-modeling/external-datasources.mdx b/content/docs/data-modeling/external-datasources.mdx
index 1b950f860d..bb879d9ffd 100644
--- a/content/docs/data-modeling/external-datasources.mdx
+++ b/content/docs/data-modeling/external-datasources.mdx
@@ -72,6 +72,7 @@ and `external.columnMap`.
```typescript
export const Customer = ObjectSchema.create({
name: 'ext_customer',
+ sharingModel: 'private',
datasource: 'warehouse',
external: {
remoteName: 'customers', // remote TABLE name (object name may differ)
@@ -255,6 +256,10 @@ ObjectSchema.create({ /* ... */ external: { remoteName: 'orders', writable: true
With either gate off, insert/update/delete on the federated object is rejected.
+The `/* ... */` above still has to carry a `sharingModel` — a custom (non-`sys_`) object
+that declares no OWD is an **`os validate` error** (`security-owd-unset`), federated or
+not. See the full object above.
+
**This gate is federation-only — it does nothing on a managed datasource.**
`allowWrites` answers *who owns this external database*, not *is this connection
diff --git a/content/docs/data-modeling/field-types.mdx b/content/docs/data-modeling/field-types.mdx
index a3da1c1714..a0b9c3c18a 100644
--- a/content/docs/data-modeling/field-types.mdx
+++ b/content/docs/data-modeling/field-types.mdx
@@ -314,7 +314,7 @@ Reference to a record in another object (foreign key).
| Property | Type | Default | Description |
|:---|:---|:---|:---|
| `reference` | `string` | **required** | Target object name (snake_case) |
-| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property (unknown keys are stripped by the schema). Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) |
+| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property. `FieldSchema` is a strict object, so an unknown key is **rejected with guidance**, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) |
| `deleteBehavior` | `'restrict' \| 'cascade' \| 'set_null'` | `'set_null'` | Behavior when referenced record is deleted. On a *required* lookup `set_null` is escalated to `restrict`, since a NOT NULL foreign key cannot be cleared — **whether the `set_null` was defaulted or written out explicitly**. On a `multiple: true` required lookup the escalation is judged per referencing row: only a row the member removal would leave EMPTY is refused. `cascade` and `restrict` are the values honored as written. Where `set_null` does run, a `multiple: true` lookup loses only the deleted **member** — the other members are kept, and a set emptied that way is stored as `[]`, never `null` |
```typescript
@@ -336,7 +336,7 @@ Parent-child relationship (cascading delete by default).
| Property | Type | Default | Description |
|:---|:---|:---|:---|
| `reference` | `string` | **required** | Target (master) object name |
-| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property (unknown keys are stripped by the schema). Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) |
+| `referenceFilters` | `string[]` | — | **Removed** (#2377, ADR-0049) — no longer a recognized field property. `FieldSchema` is a strict object, so an unknown key is **rejected with guidance**, not silently stripped (ADR-0078): the error echoes the offending key and prescribes the replacement. Use structured `lookupFilters` + `dependsOn` instead; see [Relationships](/docs/data-modeling/relationships) |
| `deleteBehavior` | `'restrict' \| 'cascade' \| 'set_null'` | `'cascade'` | Behavior when parent is deleted. `restrict` is the only value that deviates: master-detail cascades on everything else, so an explicit `set_null` here is **not** honored — the child is deleted with the parent |
| `inlineEdit` | `boolean \| 'grid' \| 'form'` | — | Edit child records inline on the parent create/edit form (`true` = auto-pick, `'grid'`, or `'form'`) |
| `inlineColumns` | `array` | — | Optional explicit inline grid columns |
@@ -530,7 +530,7 @@ Name-keyed map of embedded sub-objects (`Record`). Insertion
## Enhanced Types
### `location`
-Geographic coordinates. Stored as `{ latitude, longitude, altitude?, accuracy? }` (latitude −90..90, longitude −180..180). No per-type config properties.
+Geographic coordinates. Stored as `{ lat, lng, altitude?, accuracy? }` (`lat` −90..90, `lng` −180..180). No per-type config properties. The key names are `lat`/`lng`, not `latitude`/`longitude` — see `LocationValueSchema` in `field-value.zod.ts` (ADR-0104 D1).
```typescript
{ name: 'headquarters', label: 'Location', type: 'location' }
diff --git a/content/docs/data-modeling/fields.mdx b/content/docs/data-modeling/fields.mdx
index d14374b6b8..6fa5b294d2 100644
--- a/content/docs/data-modeling/fields.mdx
+++ b/content/docs/data-modeling/fields.mdx
@@ -17,6 +17,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Contact = ObjectSchema.create({
name: 'contact',
+ sharingModel: 'private',
label: 'Contact',
fields: {
first_name: Field.text({ label: 'First Name', required: true }),
diff --git a/content/docs/data-modeling/formulas.mdx b/content/docs/data-modeling/formulas.mdx
index 4d526d1190..43c0cc7126 100644
--- a/content/docs/data-modeling/formulas.mdx
+++ b/content/docs/data-modeling/formulas.mdx
@@ -93,6 +93,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Invoice = ObjectSchema.create({
name: 'invoice',
+ sharingModel: 'private',
nameField: 'display_title', // ADR-0079 — the record title is a designated field; composite titles migrate off the deprecated `titleFormat` to a text formula
fields: {
// Composite record title as a text formula, surfaced via `nameField` above.
diff --git a/content/docs/data-modeling/index.mdx b/content/docs/data-modeling/index.mdx
index 8e8782127f..65309177e9 100644
--- a/content/docs/data-modeling/index.mdx
+++ b/content/docs/data-modeling/index.mdx
@@ -15,6 +15,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Lead = ObjectSchema.create({
name: 'crm_lead',
+ sharingModel: 'public_read_write', // OWD — required on every custom object
label: 'Lead',
pluralLabel: 'Leads',
icon: 'funnel',
@@ -35,8 +36,8 @@ That one definition is enough to get a persisted table, CRUD + query endpoints,
- **A full spectrum of field types** — from `text`, `currency`, and `lookup`/`master_detail` relationships to `formula`, `summary`, `signature`, and `vector` (with `dimensions` config for AI embeddings). See the [Field Types gallery](/docs/data-modeling/field-types).
- **Validation as metadata** — required/format rules, CEL script validation with access to `previous.`, uniqueness via indexes, and severity levels — enforced identically in API, UI, and automation.
- **CEL expressions** — formula fields and computed defaults share one expression language ([Expressions](/docs/data-modeling/formulas)).
-- **A compiled query AST** — queries are JSON documents validated against the protocol, then compiled by a driver into native queries with joins, aggregations, window functions, HAVING, and subqueries. The [query cheat sheet](/docs/data-modeling/queries) covers the syntax; the [spec](/docs/protocol/objectql/query-syntax) is normative.
-- **Four database drivers in this repo** — `driver-sql` (PostgreSQL / MySQL / SQLite via Knex), `driver-mongodb`, `driver-memory` (in-memory, for tests and demos), and `driver-sqlite-wasm` (SQLite in the browser / WebContainers). The same model runs unchanged on any of them.
+- **A compiled query AST** — queries are JSON documents validated against the protocol, then compiled by a driver into native queries with aggregations, `groupBy`, `HAVING`, and subqueries. `joins` and request-surface `windowFunctions` were **retired in protocol 17** (#4286, ADR-0049) and are now rejected at parse; window functions survive only as a SQL-driver door (`SqlDriver.findWithWindowFunctions()`), not on the `IDataDriver` contract. The [query cheat sheet](/docs/data-modeling/queries) covers the syntax; the [spec](/docs/protocol/objectql/query-syntax) is normative.
+- **Five database drivers in this repo** — `driver-sql` (PostgreSQL / MySQL / SQLite via Knex), `driver-mongodb`, `driver-memory` (in-memory, for tests and demos), `driver-sqlite-wasm` (SQLite in the browser / WebContainers), and `driver-turso` (Turso / libSQL — an **optional** install, because it pulls `@libsql/client` plus native bindings; see [Database Drivers](/docs/data-modeling/drivers)). The same model runs unchanged on any of them.
- **External datasource federation** — introspect an existing external database, import selected tables into the catalog, and query them alongside native objects ([External Datasources](/docs/data-modeling/external-datasources)).
- **Composable ownership** — one package owns an object, and any other package can merge fields, validation rules, and indexes into it without forking the definition ([Object Extensions](/docs/data-modeling/object-extensions)).
- **Repeatable bulk import** — a named mapping projects someone else's column headers and codes onto your fields, and the import endpoint applies it by name ([Import Mappings](/docs/data-modeling/import-mappings)).
diff --git a/content/docs/data-modeling/objects.mdx b/content/docs/data-modeling/objects.mdx
index ed08813c1e..06ea4cbaaf 100644
--- a/content/docs/data-modeling/objects.mdx
+++ b/content/docs/data-modeling/objects.mdx
@@ -15,6 +15,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Account = ObjectSchema.create({
name: 'account',
+ sharingModel: 'private',
label: 'Account',
pluralLabel: 'Accounts',
icon: 'building',
@@ -79,7 +80,7 @@ Control which platform features are active for this object:
enable: {
trackHistory: true, // Field history tracking for audit
searchable: true, // Include in global search index
- apiEnabled: true, // Expose via REST/GraphQL APIs
+ apiEnabled: true, // Expose object via automatic APIs (REST)
apiMethods: ['get', 'list', 'create', 'update', 'delete'],
files: true, // File attachments
feeds: true, // Activity feed and comments
@@ -373,6 +374,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const ProjectTask = ObjectSchema.create({
name: 'project_task',
+ sharingModel: 'private',
label: 'Project Task',
pluralLabel: 'Project Tasks',
icon: 'check-square',
diff --git a/content/docs/data-modeling/schema-design.mdx b/content/docs/data-modeling/schema-design.mdx
index 15b333f210..c2bed2ca48 100644
--- a/content/docs/data-modeling/schema-design.mdx
+++ b/content/docs/data-modeling/schema-design.mdx
@@ -21,6 +21,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const MyObject = ObjectSchema.create({
// Metadata
name: 'my_object', // Machine name (snake_case)
+ sharingModel: 'private', // OWD baseline — REQUIRED on custom objects
label: 'My Object', // Display name
pluralLabel: 'My Objects', // Plural form
icon: 'briefcase', // Icon identifier
@@ -67,13 +68,14 @@ Control which features are available for an object:
enable: {
trackHistory: true, // Track field changes over time
searchable: true, // Include in global search
- apiEnabled: true, // Expose via REST/GraphQL
+ apiEnabled: true, // Expose object via automatic APIs (REST)
apiMethods: [ // Whitelist API operations — six primitives only;
'get', // search/export/… DERIVE from them (list grants
'list', // aggregate/search/export; create+update grants
'create', // upsert/import). undefined = all, [] = none.
'update',
- 'delete'
+ 'delete',
+ 'bulk' // omitting `bulk` DENIES batch operations
],
files: true, // Allow file attachments
feeds: true, // Enable activity feed (Chatter-like)
@@ -284,6 +286,7 @@ import { ObjectSchema } from '@objectstack/spec/data';
export const Account = ObjectSchema.create({
name: 'account',
+ sharingModel: 'private',
label: 'Account',
fieldGroups: [
@@ -403,6 +406,7 @@ here (ADR-0085).
```typescript
export const Account = ObjectSchema.create({
name: 'account',
+ sharingModel: 'private',
label: 'Account',
pluralLabel: 'Accounts',
icon: 'building',
diff --git a/content/docs/data-modeling/seed-data.mdx b/content/docs/data-modeling/seed-data.mdx
index 41b00eb40c..6a08f1b002 100644
--- a/content/docs/data-modeling/seed-data.mdx
+++ b/content/docs/data-modeling/seed-data.mdx
@@ -246,6 +246,7 @@ seed value is an **array of natural keys**. Each element resolves independently.
```typescript
const Book = ObjectSchema.create({
name: 'book',
+ sharingModel: 'private',
fields: {
name: Field.text({ label: 'Title', required: true }),
authors: Field.lookup('author', { label: 'Authors', multiple: true }),
diff --git a/content/docs/data-modeling/validation-rules.mdx b/content/docs/data-modeling/validation-rules.mdx
index 4bbb72f7d9..ef13e81639 100644
--- a/content/docs/data-modeling/validation-rules.mdx
+++ b/content/docs/data-modeling/validation-rules.mdx
@@ -91,6 +91,15 @@ These properties apply to **all** field types and are validated by the base `Fie
**Default constraints:** Validated the same as `text` (`maxLength`/`minLength` only). On a generic (non-`better-auth`) object a `password`-typed value is stored **plaintext at rest** but **masked to `••••••••` on read** through the normal query path (ADR-0100) — the engine does not hash or encrypt it. One-way hashing and verification of real login credentials are owned entirely by the auth subsystem (better-auth); its own credential column (`sys_account.password`) is a hashed `Field.text()` column that is exempt from this masking. For a reversible, encrypted-at-rest value on your own objects, use the `secret` type instead.
+### `secret`
+
+| Property | Type | Default | Validation Behavior |
+|:---|:---|:---|:---|
+| `maxLength` | `number` | — | Maximum length of the cleartext value |
+| `minLength` | `number` | — | Minimum length of the cleartext value |
+
+**Default constraints:** Reversible, encrypted-at-rest value (DB password, API key, token) — ADR-0100. **Fail-closed:** with no `ICryptoProvider` registered, a write **throws** rather than persisting cleartext. The value is encrypted on write into a `sys_secret` row, only an opaque handle is persisted on the record, and reads are masked. Distinct from `password`, which is plaintext at rest (or one-way hashed inside the auth subsystem).
+
---
## Rich Content Types
@@ -144,7 +153,7 @@ These properties apply to **all** field types and are validated by the base `Fie
| `currencyConfig.currencyMode` | `enum` | `dynamic` | `dynamic` (user-selectable) or `fixed` (single currency) |
| `currencyConfig.defaultCurrency` | `string` | `CNY` | 3-character currency code (ISO 4217 or crypto) |
-**Default constraints:** Stored as `{ value, currency }` pair. Precision defaults to 2 decimal places.
+**Default constraints:** Stored as a **bare number** (a finite numeric scalar — `valueSchemaFor` routes `currency` to `z.number().finite()`); there is no `{ value, currency }` envelope on the value path. The per-record currency **code** is a separate concern, carried by `currencyConfig` above. Precision defaults to 2 decimal places.
### `percent`
@@ -264,6 +273,15 @@ update that omits the field never fails this check, so legacy rows stay editable
application-level emptiness check is needed.
+### `user`
+
+| Property | Type | Default | Validation Behavior |
+|:---|:---|:---|:---|
+| `multiple` | `boolean` | `false` | `true` stores a JSON array of user ids |
+| `defaultValue` | `'current_user'` | — | Stamps the acting user's id on insert |
+
+**Default constraints:** Person picker — a `lookup` specialized to the built-in `sys_user` object, so `reference` is implied and must not be authored. Stored as a foreign key to `sys_user.id` and resolved through the same `$expand` machinery as `lookup`; with `multiple: true` the stored value is a JSON array of ids.
+
### `master_detail`
| Property | Type | Default | Validation Behavior |
@@ -366,11 +384,32 @@ application-level emptiness check is needed.
---
+## Embedded Structured Types
+
+These types store structured values as JSON on the parent row — no separate table and no
+foreign key. Sub-field shapes are declared on the field, and the stored value is validated
+as an open object map (`valueSchemaFor`), so sub-keys are not constrained by the field type
+itself.
+
+### `composite`
+
+**Default constraints:** Single embedded sub-object. Stored as a JSON object map (`Record`). No per-type config properties.
+
+### `repeater`
+
+**Default constraints:** Repeating embedded sub-object array. Stored as a JSON **array** of object maps (`Array>`); a non-array value is rejected. No per-type config properties.
+
+### `record`
+
+**Default constraints:** Name-keyed map of embedded sub-objects (`Record`) — ADR-0007. Stored as a JSON object map; insertion order is display order. No per-type config properties.
+
+---
+
## Enhanced Types
### `location`
-**Default constraints:** Stored as `{ latitude, longitude, altitude?, accuracy? }`. Latitude: -90 to 90. Longitude: -180 to 180. No per-type config properties.
+**Default constraints:** Stored as `{ lat, lng, altitude?, accuracy? }` — the keys are `lat`/`lng`, not `latitude`/`longitude` (`LocationValueSchema`, ADR-0104 D1). `lat`: -90 to 90. `lng`: -180 to 180. No per-type config properties.
### `address`
@@ -483,6 +522,7 @@ section above). See the
| `url` | — | Valid URL with protocol |
| `phone` | — | Permissive character set, not strict E.164 |
| `password` | — | Validated like `text`; masked on read but plaintext at rest (no hashing/encryption) |
+| `secret` | — | Encrypted at rest via `sys_secret`, masked on read; **fail-closed** — writes throw with no `ICryptoProvider` |
| `markdown` | — | `maxLength` |
| `html` | — | Sanitized, `maxLength` |
| `richtext` | — | Sanitized, `maxLength` |
@@ -499,6 +539,7 @@ section above). See the
| `radio` | `options` | Single value from options |
| `checkboxes` | `options` | Array of valid option values |
| `lookup` | `reference` | Foreign key integrity |
+| `user` | — | Lookup specialized to `sys_user`; `multiple: true` stores an id array |
| `master_detail` | `reference` | Cascade delete, ownership |
| `tree` | `reference` | Self-referencing; no automatic cycle check |
| `image` | — | Common image MIME types; `multiple` for many |
@@ -509,6 +550,9 @@ section above). See the
| `formula` | `expression` | Read-only, computed at runtime |
| `summary` | `summaryOperations` | Read-only, roll-up from children |
| `autonumber` | — | Read-only, auto-incremented |
+| `composite` | — | Single embedded sub-object; stored as a JSON object map |
+| `repeater` | — | Embedded sub-object array; stored as a JSON array of object maps |
+| `record` | — | Name-keyed map of embedded sub-objects (ADR-0007) |
| `location` | — | Lat: -90–90, Lng: -180–180 |
| `address` | — | Structured object (street, city, …) |
| `code` | — | Plain text, `language` for highlighting |
diff --git a/content/docs/data-modeling/validation.mdx b/content/docs/data-modeling/validation.mdx
index 52c7e26a59..5b81813359 100644
--- a/content/docs/data-modeling/validation.mdx
+++ b/content/docs/data-modeling/validation.mdx
@@ -1,6 +1,6 @@
---
title: Validation Metadata
-description: Define data integrity rules — formula conditions, uniqueness, format, state machine transitions, and more
+description: Define data integrity rules — formula conditions, format, cross-field checks, state machine transitions, and more
---
# Validation Metadata
@@ -10,7 +10,7 @@ description: Define data integrity rules — formula conditions, uniqueness, for
Three patterns that look like validation rules are deliberately **not** rule types, because each needs I/O or is a client-side concern. Use the layer that already does each one correctly:
-- **Uniqueness** → a unique index (`ObjectSchema.indexes`, `{ fields, unique: 'organization' | 'global' }` — state the scope, [ADR-0120](/docs/data-modeling/indexing); `partial` for a scoped constraint) or field-level `unique`. A SELECT-then-INSERT rule is inherently racy (TOCTOU); a DB unique constraint is not.
+- **Uniqueness** → a unique index (`ObjectSchema.indexes`, `{ fields, unique: 'organization' | 'global' }` — state the scope, [ADR-0120](/docs/data-modeling/indexing)) or field-level `unique`. A SELECT-then-INSERT rule is inherently racy (TOCTOU); a DB unique constraint is not.
- **Async / remote validation** → a client-form concern, and an SSRF/latency hazard on the server write path. Keep it in the form layer, or enforce the invariant with a `unique` index / lifecycle hook.
- **Custom handler** → a `beforeInsert` / `beforeUpdate` lifecycle hook, the supported extension point for arbitrary validation code.
- **Delete-time guards** → a `beforeDelete` lifecycle hook. Validation rules run only on insert/update (a delete carries no record payload to validate), so there is no `'delete'` validation event — block or gate deletions from a `beforeDelete` hook.
@@ -26,6 +26,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Order = ObjectSchema.create({
name: 'order',
+ sharingModel: 'private',
label: 'Order',
fields: {
amount: Field.currency({ label: 'Amount', required: true }),
@@ -132,10 +133,15 @@ email: Field.email({ label: 'Contact Email', unique: 'organization' }),
// Composite / scoped uniqueness via ObjectSchema.indexes
indexes: [
{ fields: ['code'], unique: 'organization' },
- // `partial` expresses a scoped/conditional constraint
]
```
+`type` and `partial` were **retired at protocol 17** (#5248, #4943) — `IndexSchema` is a
+strict object declaring exactly `name` / `fields` / `unique`, so authoring either is now a
+rejection with guidance, not a silent strip. A **partial** index is built at the database
+layer (a runtime migration issuing `CREATE UNIQUE INDEX … WHERE`), not on the declaration
+surface; see [Objects](/docs/data-modeling/objects).
+
### Format Validation
Validate against a regex pattern or standard format:
diff --git a/content/docs/getting-started/examples.mdx b/content/docs/getting-started/examples.mdx
index 47bcb0878d..5688ebd21f 100644
--- a/content/docs/getting-started/examples.mdx
+++ b/content/docs/getting-started/examples.mdx
@@ -51,12 +51,24 @@ The fastest way to explore all examples at once:
```bash
git clone https://github.com/objectstack-ai/objectstack.git
cd objectstack
-pnpm install
+pnpm install # Node 22+, pnpm 8+ (corepack enable)
+pnpm build # REQUIRED — nothing builds the workspace implicitly
pnpm dev:showcase # or: pnpm dev:todo / pnpm dev:crm
```
Each script starts one example's dev server. `pnpm dev` is an alias for `pnpm dev:showcase`.
+
+ **`pnpm build` is not optional.** The root `dev:*` scripts begin with
+ `node scripts/check-dev-prereqs.mjs`, which refuses to boot an unbuilt workspace —
+ every package's `dist/` entry point has to be on disk. There is no `postinstall`, so
+ `pnpm install` alone leaves the workspace unbuilt. This is the same sequence
+ `README.md` documents.
+
+ `pnpm build` still does not produce the Console SPA at `/_console/` — that is built
+ separately by `pnpm objectui:build`.
+
+
---
## app-todo — Your First App
@@ -105,6 +117,9 @@ examples/app-todo/
```bash
+# From a freshly cloned monorepo, build it once first — `objectstack dev` runs
+# against the workspace's dist/ output and the root prereq gate does not guard
+# this path: pnpm install && pnpm build
cd examples/app-todo
pnpm dev
```
@@ -127,6 +142,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
export const Task = ObjectSchema.create({
name: 'todo_task',
+ sharingModel: 'public_read_write', // OWD — required on every custom object
label: 'Task',
pluralLabel: 'Tasks',
icon: 'check-square',
@@ -281,6 +297,9 @@ profiles, translations, themes, webhooks, and more. Use it as a living reference
when you want to see how a particular metadata type is authored.
```bash
+# From a freshly cloned monorepo, build it once first — `objectstack dev` runs
+# against the workspace's dist/ output and the root prereq gate does not guard
+# this path: pnpm install && pnpm build
cd examples/app-showcase
pnpm dev
```
diff --git a/content/docs/getting-started/index.mdx b/content/docs/getting-started/index.mdx
index 6c5b3dc396..c8ceaadfe7 100644
--- a/content/docs/getting-started/index.mdx
+++ b/content/docs/getting-started/index.mdx
@@ -96,7 +96,7 @@ Think of ObjectStack as:
- **Kubernetes** for business applications - Declarative configuration over imperative code
- **Terraform** for data modeling - Infrastructure as code, but for data
-- **GraphQL + React Server Components** - Schema-driven data + UI rendering combined (REST ships today; GraphQL is exposed via the `IGraphQLService` contract)
+- **GraphQL + React Server Components** - Schema-driven data + UI rendering combined — as an analogy for the shape, not the transport. The generated data surface is **REST**; GraphQL is not in the product plan and `/graphql` was removed from the dispatcher.
- **MCP for business systems** - Structured, permission-aware tools generated from metadata
## Key Features
@@ -130,7 +130,7 @@ Think of ObjectStack as:
| Traditional Approach | ObjectStack Approach |
| :--- | :--- |
| Write SQL migrations manually | Schema changes sync automatically |
-| Build CRUD APIs by hand | REST generated from schema (GraphQL via the `IGraphQLService` contract) |
+| Build CRUD APIs by hand | REST generated from schema |
| Manually define agent tools | MCP/tool surfaces generated from metadata |
| Duplicate validation logic 3x | Define once, enforce everywhere |
| Lock into one database vendor | Swap databases without code changes |
diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx
index 766d7e5b44..d3246d49f8 100644
--- a/content/docs/ui/actions.mdx
+++ b/content/docs/ui/actions.mdx
@@ -193,11 +193,16 @@ is collected, then the body runs with those values as its `input`.
Surfaces can also reference actions **by name**:
```typescript
-// List views — row and bulk menus
+// List views — row and bulk menus. These are LIST-VIEW keys, so they nest
+// under `list` (or a `listViews` entry) — never at the container top level:
+// `ViewSchema` is a strict object and rejects them there.
defineView({
// ...
- rowActions: ['complete_task'],
- bulkActions: ['showcase_bulk_reassign'],
+ list: {
+ // ...
+ rowActions: ['complete_task'],
+ bulkActions: ['showcase_bulk_reassign'],
+ },
});
// Record pages — a quick-actions bar
diff --git a/content/docs/ui/create-vs-edit-form.mdx b/content/docs/ui/create-vs-edit-form.mdx
index a77b8244bc..3e6314731a 100644
--- a/content/docs/ui/create-vs-edit-form.mdx
+++ b/content/docs/ui/create-vs-edit-form.mdx
@@ -79,7 +79,7 @@ export const ContactViews = defineView({
create: {
type: 'simple', data, title: 'New contact',
sections: [
- { label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] },
+ { name: 'who', label: 'Who is this?', columns: 1, fields: ['name', 'email', 'phone', 'company'] },
],
},
},
diff --git a/content/docs/ui/public-data-collection.mdx b/content/docs/ui/public-data-collection.mdx
index 14fddf5e3a..0d9e91a411 100644
--- a/content/docs/ui/public-data-collection.mdx
+++ b/content/docs/ui/public-data-collection.mdx
@@ -21,7 +21,7 @@ formViews: {
type: 'simple',
data: { provider: 'object', object: 'showcase_inquiry' },
sections: [
- { label: 'Tell us about yourself', columns: 1, fields: [
+ { name: 'about_you', label: 'Tell us about yourself', columns: 1, fields: [
{ field: 'name', required: true },
{ field: 'email', required: true },
{ field: 'company' },
diff --git a/content/docs/ui/views.mdx b/content/docs/ui/views.mdx
index 2a845fbe1b..168ba44b20 100644
--- a/content/docs/ui/views.mdx
+++ b/content/docs/ui/views.mdx
@@ -50,7 +50,7 @@ export const TaskViews = defineView({
type: 'simple',
data,
sections: [
- { label: 'Task', columns: 2, fields: ['title', 'status', 'assignee', 'due_date'] },
+ { name: 'task', label: 'Task', columns: 2, fields: ['title', 'status', 'assignee', 'due_date'] },
],
},
},
@@ -388,7 +388,7 @@ formViews: {
| Property | Type | Description |
| :--- | :--- | :--- |
-| `name` | `string` | Stable identifier (snake_case) for i18n lookup |
+| `name` | `string` | Stable identifier (snake_case) for i18n lookup — resolves `objects.{object}._sections.{name}.label`. **A nameless section renders its authored label in every locale**, so give every section a `name` if the app is localized |
| `label` | `string` | Section header |
| `columns` | `1-4` | Grid column count |
| `collapsible` | `boolean` | Can section be collapsed |