Skip to content

Commit f1f9100

Browse files
committed
docs(data-modeling): document the managedBy lifecycle bucket and ADR-0103 write policy
The Object reference never documented `managedBy` at all. Add a `managedBy` + `userActions` row to the object property table and a "Lifecycle bucket" subsection covering the five buckets, the engine-owned vs admin/user-writable `system` split (ADR-0103), the resolved-affordance write policy, the `assertEngineOwnedWriteAllowed` guard, and the apiMethods reconciliation. - fields.mdx: `readonly`'s "governed by their own guards" now names the resolved-affordance write policy and links to the bucket section. - authentication.mdx: note the better-auth identity objects are an identity-owned bucket — user-context generic CRUD is suppressed, better-auth's own writes run as system context. Docs only. Refs #3220 / ADR-0092 / ADR-0103. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fp9yZxRQ3mb7p4vVwqFXKE
1 parent 9d897b3 commit f1f9100

3 files changed

Lines changed: 54 additions & 1 deletion

File tree

content/docs/data-modeling/fields.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ These properties are available on all field types:
284284
| `description` | `string` || Developer documentation |
285285
| `inlineHelpText` | `string` || Help text shown in UI |
286286
| `hidden` | `boolean` | `false` | Hide from default views |
287-
| `readonly` | `boolean` | `false` | Prevent editing — hidden from create/edit forms AND server-enforced on both write paths: a non-system write to the field is silently dropped on `UPDATE` (in the engine) and on `INSERT` through the data API (REST/GraphQL/MCP/import, at the DataProtocol ingress). A stripped field still falls back to its `defaultValue`; **seeding a `readonly` column at create requires a system context** (import/migration/programmatic seed). Platform (`sys_`/`managedBy`) objects are governed by their own guards instead. |
287+
| `readonly` | `boolean` | `false` | Prevent editing — hidden from create/edit forms AND server-enforced on both write paths: a non-system write to the field is silently dropped on `UPDATE` (in the engine) and on `INSERT` through the data API (REST/GraphQL/MCP/import, at the DataProtocol ingress). A stripped field still falls back to its `defaultValue`; **seeding a `readonly` column at create requires a system context** (import/migration/programmatic seed). Platform (`sys_`/`managedBy`) objects are governed by their own write policy instead — the resolved-affordance write guard keyed off the object's [lifecycle bucket](/docs/data-modeling/objects#lifecycle-bucket-managedby) (ADR-0103), not this field-level flag. |
288288
| `sortable` | `boolean` | `true` | Allow sorting by this field |
289289
| `group` | `string` || Group name for organizing in forms (e.g. `'billing'`) |
290290

content/docs/data-modeling/objects.mdx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,61 @@ indexes: [
217217
| Property | Type | Description |
218218
| :--- | :--- | :--- |
219219
| `isSystem` | `boolean` | System object, protected from deletion (default: `false`) |
220+
| `managedBy` | `enum` | Lifecycle bucket that sets the default CRUD affordances and write policy — `'platform'` (default), `'config'`, `'system'`, `'append-only'`, `'better-auth'`. See [Lifecycle bucket](#lifecycle-bucket-managedby) below. |
221+
| `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](#lifecycle-bucket-managedby). |
220222
| `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) |
221223
| `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. |
222224
| `validations` | `ValidationRule[]` | Object-level validation rules (see [Validation](/docs/data-modeling/validation)) |
223225

226+
### Lifecycle bucket (`managedBy`)
227+
228+
`managedBy` declares which lifecycle bucket an object belongs to. It sets the
229+
**default** CRUD affordances the UI renders and the write policy the platform
230+
enforces. The enforced policy is the *resolved affordance* — the bucket default
231+
with any `userActions` override applied (`resolveCrudAffordances`) — **not** the
232+
bare bucket string.
233+
234+
| Bucket | Default write policy |
235+
| :--- | :--- |
236+
| `platform` | **Default.** User-owned business data — full New / Import / Edit / Delete. |
237+
| `config` | Admin-authored configuration — New / Edit / Delete, no CSV import. |
238+
| `system` | Platform-defined schema. **Engine-owned by default**: a platform service owns the row lifecycle, generic CRUD is hidden, and the object is exposed `['get', 'list']` only. |
239+
| `append-only` | Immutable audit trail — View + Export only. |
240+
| `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). |
241+
242+
**Engine-owned vs. admin-writable `system` objects (ADR-0103).** The `system`
243+
bucket covers two kinds of object. Most are *engine-owned* — jobs,
244+
notifications, approval rows, `sys_record_share`, `sys_automation_run`, audit
245+
trails, `sys_secret` — written only by their owning service under a system
246+
context, never through the generic `/data` API; a fail-closed guard
247+
(`assertEngineOwnedWriteAllowed`) rejects user-context generic writes to them.
248+
A minority are *platform-defined schema with admin/user-writable data* — the
249+
RBAC link tables, `sys_user_preference`, `sys_approval_delegation`, the
250+
messaging config grids. These keep `managedBy: 'system'` but declare
251+
`userActions` to open the writes they legitimately take:
252+
253+
```typescript
254+
export const SysUserPreference = ObjectSchema.create({
255+
name: 'sys_user_preference',
256+
managedBy: 'system',
257+
// Affordance only — RLS / delegated administration is the actual authz.
258+
userActions: { create: true, edit: true, delete: true },
259+
//
260+
});
261+
```
262+
263+
The same override works on `append-only`. `userActions` is an *affordance*
264+
declaration; the real authorization for these rows is still enforced by RLS,
265+
delegated administration, and permission sets.
266+
267+
<Callout type="warn">
268+
A managed object may not advertise `enable.apiMethods` verbs its resolved
269+
affordances forbid — the registry strips the contradiction at registration
270+
(`reconcileManagedApiMethods`, ADR-0049). To expose a generic write verb on a
271+
`system`/`append-only` object, declare the matching `userActions` rather than
272+
listing the verb in `apiMethods`.
273+
</Callout>
274+
224275
## Naming Conventions
225276

226277
| Element | Convention | Example |

content/docs/permissions/authentication.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ The plugin creates the following auth objects (using ObjectStack `sys_` protocol
170170
> automatically maps these to `sys_`-prefixed protocol names via `AUTH_MODEL_TO_PROTOCOL`.
171171
> Client-side API routes (`/api/v1/auth/*`) are **not affected** — they do not expose object names.
172172
173+
These objects are `managedBy: 'better-auth'` (an identity-owned [lifecycle bucket](/docs/data-modeling/objects#lifecycle-bucket-managedby)): generic user-context CRUD through the `/data` API is suppressed — a fail-closed identity write guard (ADR-0092/0103) rejects direct writes so password hashing, session validation, and verification flows can't be bypassed. better-auth's own writes run under a **system context** and pass through. Mutate these records through the sign-in, invitation, and security flows above, never by writing the objects directly.
174+
173175
---
174176

175177
## Authentication Methods

0 commit comments

Comments
 (0)