Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/organization-field-pin-annotation-prose-refresh.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
---
'@objectstack/spec': patch
---

Refresh the `tenancy.organizationField` scope-pin annotation and its
`.describe()` in `packages/spec/src/data/object.zod.ts` — prose accuracy
only, no schema or behaviour change.

The #8778 scope-pin annotation (landed by #10999) said consumer 1 (audit
stamping) was "as of this annotation, still the only one wired up" and that
consumers 2 and 3 (the approval-row writer and the automation-run recorder)
were "sanctioned but not yet implemented". #10101's PR #11311 (merged
2026-08-23) landed both: `resolveRecordOrganizationField` was promoted to a
shared resolver in `@objectstack/metadata-core` (plugin-audit re-exports it
from its original path) and all three sanctioned platform-row writers now
call it. The annotation and the `.describe()` string are updated to name all
three live consumers; the pin's load-bearing property — "The ruling sanctions
exactly THREE consumers of this key, and no others", a fourth consumer needs
its own ruling — is unchanged and still stated verbatim.

No accept/reject behaviour change, no schema shape change, no new keys.
2 changes: 1 addition & 1 deletion content/docs/references/data/object.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -314,7 +314,7 @@ Boolean-or-predicates override for a built-in CRUD affordance.
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | ✅ | Enable multi-tenancy for this object |
| **tenantField** | `string` | optional | Column this object is tenant-scoped by. Omit it unless the tenant column genuinely is not the platform's: when undeclared the driver falls back to `organization_id`, the kernel-injected column the RLS predicates and `tenantPolicy()` also assume. A declared name is honoured only when the object really has that field — otherwise the same `organization_id` fallback applies. No default is materialized here on purpose (#5315). |
| **organizationField** | `string` | optional | STAMP-ONLY (#8778): column carrying the organization a row is ABOUT, consulted exclusively when audit rows are stamped. It does NOT tenant-scope anything — no read path (`applyTenantScope`, `injectTenantOnInsert`, `computeTenantLayer0Filter`) reads it, so declaring it never walls the object and never hides rows. Declare it only when the organization a row belongs to lives under a column that deliberately is NOT the tenant column: `sys_api_key` is the shipped example — a credential table that must stay unwalled (`enabled: false`) while history/revocation audit rows stamp the organization of the key they describe (`active_organization_id`). Ordinary tenant objects omit it; their stamp column is resolved from `tenantField` / `organization_id` already. Honoured only when the object really has the field, like `tenantField`. |
| **organizationField** | `string` | optional | STAMP-ONLY (#8778, widened by cloud#1395): column carrying the organization a row is ABOUT, consulted by the three sanctioned platform-row writers — audit stamping, the approval-row writer (`plugin-approvals`), and the automation-run recorder (`service-automation`) — via the shared `resolveRecordOrganizationField` resolver in `@objectstack/metadata-core`. It does NOT tenant-scope anything — no read path (`applyTenantScope`, `injectTenantOnInsert`, `computeTenantLayer0Filter`) reads it, so declaring it never walls the object and never hides rows. Declare it only when the organization a row belongs to lives under a column that deliberately is NOT the tenant column: `sys_api_key` is the shipped example — a credential table that must stay unwalled (`enabled: false`) while history/revocation audit rows stamp the organization of the key they describe (`active_organization_id`). Ordinary tenant objects omit it; their stamp column is resolved from `tenantField` / `organization_id` already. Honoured only when the object really has the field, like `tenantField`. |


---
Expand Down
49 changes: 29 additions & 20 deletions packages/spec/src/data/object.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -591,12 +591,14 @@ const TENANCY_MODES_EXPLAINER =
* credential tables they deliberately do not — `sys_api_key` records the
* organization a key authenticates into under `active_organization_id`
* precisely so the credential table does NOT become org-walled (#8287). The
* key is consulted exclusively by audit stamping (plugin-audit's
* `resolveRecordOrganizationField`); no read path reads it, and that
* key is consulted only by platform-row stamping — the three sanctioned
* writers below, via `@objectstack/metadata-core`'s
* `resolveRecordOrganizationField` (plugin-audit re-exports it from its
* original path; public surface unchanged) — never by a read path, and that
* read-neutrality is pinned by tests beside each read path. ⛔ Scope-pinned by
* the #8778 ruling: this is ONE stamp-only declaration key, not the opening
* move of a general field-roles mechanism — a consumer other than audit
* stamping needs its own ruling before reading it.
* move of a general field-roles mechanism — a consumer other than the three
* sanctioned writers needs its own ruling before reading it.
*
* That pin is WIDENED **by name** by the maintainer ruling recorded on
* cloud#1395, 2026-08-17T03:18Z, accepting the decision-inbox recommendations
Expand All@@ -611,19 +613,22 @@ const TENANCY_MODES_EXPLAINER =
*
* The ruling sanctions exactly THREE consumers of this key, and no others:
*
* 1. **audit stamping** — plugin-audit's `resolveRecordOrganizationField`;
* the original #8778 consumer and, as of this annotation, still the only
* one wired up;
* 2. **`plugin-approvals`** — the approval-row writer;
* 3. **the automation-run recorder** — reached when
* `resolveRecordOrganizationField` is promoted to the shared platform-row
* resolver.
*
* Consumers 2 and 3 are sanctioned but not yet implemented: #10101 carries that
* behaviour change (this card is annotation-only and changes no accept/reject
* behaviour). Which is why the `.describe()` below still speaks of audit rows —
* it states what reads the key TODAY, and #10101 updates it as the readers
* actually land.
* 1. **audit stamping** — `@objectstack/metadata-core`'s
* `resolveRecordOrganizationField`; the original #8778 consumer
* (plugin-audit re-exports it from its original path; public surface
* unchanged);
* 2. **`plugin-approvals`** — the approval-row writer (`openNodeRequest`,
* the only `sys_approval_request` insert site);
* 3. **the automation-run recorder** — both write paths of
* `ObjectStoreSuspendedRunStore` in `service-automation` (`serialize()`
* for paused rows, `recordTerminal()` for terminal rows).
*
* All three are live on `main`: #10101's PR #11311 (merged 2026-08-23)
* promoted `resolveRecordOrganizationField` to the shared resolver in
* `@objectstack/metadata-core` and wired all three platform-row writers to
* it — each resolves the SUBJECT record's organization first, falling back
* to actor context, exactly as the ruling above states. The `.describe()`
* below names all three consumers accordingly.
*
* ⛔ The refusal posture is UNCHANGED for a FOURTH consumer. Three named
* platform-row writers are still not a general field-roles mechanism: anything
Expand DownExpand Up@@ -663,9 +668,13 @@ export const TenancyConfigSchema = lazySchema(() => strictObject({
'fallback applies. No default is materialized here on purpose (#5315).',
),
organizationField: z.string().optional().describe(
'STAMP-ONLY (#8778): column carrying the organization a row is ABOUT, ' +
'consulted exclusively when audit rows are stamped. It does NOT ' +
'tenant-scope anything — no read path (`applyTenantScope`, ' +
'STAMP-ONLY (#8778, widened by cloud#1395): column carrying the ' +
'organization a row is ABOUT, consulted by the three sanctioned ' +
'platform-row writers — audit stamping, the approval-row writer ' +
'(`plugin-approvals`), and the automation-run recorder ' +
'(`service-automation`) — via the shared `resolveRecordOrganizationField` ' +
'resolver in `@objectstack/metadata-core`. It does NOT tenant-scope ' +
'anything — no read path (`applyTenantScope`, ' +
'`injectTenantOnInsert`, `computeTenantLayer0Filter`) reads it, so ' +
'declaring it never walls the object and never hides rows. Declare it ' +
'only when the organization a row belongs to lives under a column that ' +
Expand Down
Loading