From f1f9100f5658e2ac968f64144a0e40500f68e314 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 20 Jul 2026 01:57:15 +0000 Subject: [PATCH] docs(data-modeling): document the managedBy lifecycle bucket and ADR-0103 write policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Fp9yZxRQ3mb7p4vVwqFXKE --- content/docs/data-modeling/fields.mdx | 2 +- content/docs/data-modeling/objects.mdx | 51 +++++++++++++++++++++ content/docs/permissions/authentication.mdx | 2 + 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/content/docs/data-modeling/fields.mdx b/content/docs/data-modeling/fields.mdx index 587e38904b..546a76b0d0 100644 --- a/content/docs/data-modeling/fields.mdx +++ b/content/docs/data-modeling/fields.mdx @@ -284,7 +284,7 @@ These properties are available on all field types: | `description` | `string` | — | Developer documentation | | `inlineHelpText` | `string` | — | Help text shown in UI | | `hidden` | `boolean` | `false` | Hide from default views | -| `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. | +| `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. | | `sortable` | `boolean` | `true` | Allow sorting by this field | | `group` | `string` | — | Group name for organizing in forms (e.g. `'billing'`) | diff --git a/content/docs/data-modeling/objects.mdx b/content/docs/data-modeling/objects.mdx index 88f119fa82..600a1934b8 100644 --- a/content/docs/data-modeling/objects.mdx +++ b/content/docs/data-modeling/objects.mdx @@ -217,10 +217,61 @@ indexes: [ | Property | Type | Description | | :--- | :--- | :--- | | `isSystem` | `boolean` | System object, protected from deletion (default: `false`) | +| `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. | +| `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). | | `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. | | `validations` | `ValidationRule[]` | Object-level validation rules (see [Validation](/docs/data-modeling/validation)) | +### Lifecycle bucket (`managedBy`) + +`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. **Engine-owned by default**: a platform service owns the row lifecycle, generic CRUD is hidden, and the object is exposed `['get', 'list']` only. | +| `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. admin-writable `system` objects (ADR-0103).** The `system` +bucket covers two kinds of object. Most are *engine-owned* — jobs, +notifications, approval rows, `sys_record_share`, `sys_automation_run`, audit +trails, `sys_secret` — 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. +A minority are *platform-defined schema with admin/user-writable data* — the +RBAC link tables, `sys_user_preference`, `sys_approval_delegation`, the +messaging config grids. These keep `managedBy: 'system'` but declare +`userActions` to open the writes they legitimately take: + +```typescript +export const SysUserPreference = 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`. + + ## Naming Conventions | Element | Convention | Example | diff --git a/content/docs/permissions/authentication.mdx b/content/docs/permissions/authentication.mdx index 4c92f9e88a..8afba819f6 100644 --- a/content/docs/permissions/authentication.mdx +++ b/content/docs/permissions/authentication.mdx @@ -170,6 +170,8 @@ The plugin creates the following auth objects (using ObjectStack `sys_` protocol > automatically maps these to `sys_`-prefixed protocol names via `AUTH_MODEL_TO_PROTOCOL`. > Client-side API routes (`/api/v1/auth/*`) are **not affected** — they do not expose object names. +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. + --- ## Authentication Methods