From f301e3ca874c10a8c14f3168772aac8fe5ad3661 Mon Sep 17 00:00:00 2001 From: os-litant Date: Thu, 10 Sep 2026 16:10:20 +0000 Subject: [PATCH] docs(skills): state what `single` posture means for the organization count in objectstack-data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The published `objectstack-data` skill presented the object-level `tenancy` block as the multi-tenancy instruction and never said that the deployment posture decides how many organizations exist. An AI author reading it (plus the runtime's exemption comment) lands on "stay `single`, seed one `sys_organization` per tenant, hand-write the RLS wall" — the shape the deployment page forbids by name. - `rules/security.md` § Multi-tenancy: the posture ↔ organization-count contract (`single` = the bootstrapped Default Organization only, a second refused 403; tenants as organizations need a walled posture declared on the deployment, open core since ADR-0132), with ADR-0093 / ADR-0105 § "Today's two postures" / the tenancy-modes page as the pointer; `tenancy` re-presented as the row-level knob within a posture. - `SKILL.md` § Seed Data & Fixtures: `sys_organization` is platform-bootstrapped and never a seed target. - `references/data-hooks.md` § Two isolation axes: "multi-org needs cloud" and "Community edition never populates an org" rewritten to the ADR-0132 open-core truth and the `single` contract. The skills token ratchet is paid in the same edit: the sentences replaced are tightened, the database-per-tenant bullet in security.md is folded away (the fact stays in data-hooks.md § Two isolation axes), and no ceiling moves. Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK Co-authored-by: Claude --- skills/objectstack-data/SKILL.md | 31 ++++++++------- .../objectstack-data/references/data-hooks.md | 8 ++-- skills/objectstack-data/rules/security.md | 39 ++++++++++--------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 48f3fdc9c4..4b4ffa9232 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -625,14 +625,17 @@ tenant-authored metadata. ## Seed Data & Fixtures (`defineSeed()`) -Object definition and its seed data live together — writing a `*.object.ts` -almost always goes with a `*.seed.ts` (test fixtures, reference rows, -bootstrap data). `defineSeed()` is type-safe: pass the object definition -and TypeScript checks every record's field keys at compile time. +Object definition and seed data live together — a `*.object.ts` usually +pairs with a `*.seed.ts` (fixtures, reference rows, bootstrap data). +`defineSeed()` is type-safe: TypeScript checks every record's field keys +against the object definition. -> The factory is named `defineSeed` — **not** `defineDataset`. The `dataset` -> name is reserved for the unrelated ADR-0021 analytics semantic layer -> (`defineDataset` from `@objectstack/spec/ui`), which is not a seed factory. +> The factory is `defineSeed` — **not** `defineDataset`, which is the +> unrelated ADR-0021 analytics semantic layer (`@objectstack/spec/ui`). + +> ⛔ `sys_organization` is platform-bootstrapped — never a seed target; the +> deployment posture decides how many exist (`rules/security.md` +> § Multi-tenancy). ### Quick start @@ -689,8 +692,8 @@ Full Zod shape: `node_modules/@objectstack/spec/src/data/seed.zod.ts`. ### `externalId` selection -Pick a stable natural business key. **Never use `id`** — UUIDs differ -across environments. +Pick a stable natural key. **Never use `id`** — UUIDs differ across +environments. | Scenario | Key | |:---------|:----| @@ -705,11 +708,11 @@ For `lookup` fields, supply the **natural key** of the target record (not its UUID). The seed runner resolves at load time. Order seeds so parents appear before children in the exported array: -> If a lookup value matches no natural key, the loader now falls back to -> resolving it as the target's `id` — so a reference to a real existing -> record by internal id resolves instead of dangling to null. Natural keys -> remain the portable default; rely on the id fallback only for records you -> didn't seed (e.g. a system user). +> If a lookup value matches no natural key, the loader falls back to +> resolving it as the target's `id` — so a reference to an existing record +> by internal id resolves instead of dangling to null. Natural keys remain +> the portable default; rely on the id fallback only for records you didn't +> seed (e.g. a system user). ```typescript const contacts = defineSeed(Contact, { diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index 7ea17981ee..8e1fa560eb 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -620,14 +620,16 @@ when a hook must work regardless of whether a user resolved. > **Two isolation axes — don't conflate them.** `organization_id` is > **org row-scoping**: many organizations share one database and every row > carries its owning org (`current_user.organizationId` filters reads/writes; -> multi-org needs cloud + `@objectstack/organizations`). That is different from +> it needs a walled posture — `OS_TENANCY_POSTURE=group|isolated` — with +> `@objectstack/organizations` declared by the app, open core since +> ADR-0132). That is different from > **environment / database-per-tenant** isolation (`service-tenant`, > `driver-turso`), where "tenant" means an entire environment/database and the > generic driver-layer `tenantId` knob can carry that environment id. The > object-metadata `tenancy.*` knob configures the *mechanism* (isolation on/off > + which column); the *value* you read and write is your `organization_id` -> column. Community edition never populates an org, so `organizationId` is -> `undefined` there. +> column. Under `single` the bootstrapped Default Organization is the only +> one, so there is nothing to scope by (`rules/security.md` § Multi-tenancy). ### `input` — Operation Parameters diff --git a/skills/objectstack-data/rules/security.md b/skills/objectstack-data/rules/security.md index bdee23925a..62b7752c4a 100644 --- a/skills/objectstack-data/rules/security.md +++ b/skills/objectstack-data/rules/security.md @@ -152,33 +152,34 @@ fields: { ## Multi-tenancy -For SaaS, set `tenancy` on the object schema for row-level tenant isolation -(the tenant field is injected on write and enforced on read). The block is -**strict** — exactly two keys: +Organization count is a **deployment posture** fact, never object metadata. +`single` (default) = one logical tenant: the bootstrapped Default Organization +only, a second refused (`403`); sub-units are business units. Tenants as +`sys_organization` rows need a walled posture (`group`/`isolated`; open core, +ADR-0132) — ⛔ never `single` + your own RLS. See ADR-0093, ADR-0105 §"Today's +two postures", https://objectstack.ai/docs/deployment/tenancy-modes. + +Within a posture, `tenancy` on the object is the row-level knob (stamped on +write, enforced on read); **strict**, two keys: ```typescript tenancy: { - enabled: true, // enable row-level tenant isolation - // tenantField — NO default; omit it and the driver uses `organization_id` + enabled: true, + // tenantField: no default — omit it ⇒ `organization_id` } ``` -- **Database-per-tenant isolation is not object metadata** — it is an - environment/deployment choice (each environment carries its own database URL). - ## Platform-global / admin-only objects (visibility posture) -Some system/config objects are **env-global** (not partitioned per org) and -should be visible to a **platform admin env-wide** but hidden from members — -e.g. identity tables a plugin writes via its own adapter (`sys_sso_provider`, -OAuth clients). These hit a non-obvious interaction: - -- Reads of a tenant object pass the **Layer 0 tenant wall** (ADR-0095 D1): an - `organization_id == ` filter AND-composed ahead of - every business RLS policy. Any row whose `organization_id` is **null or - absent** (common for adapter-written rows that never get the tenant stamp) is - **denied** — the list renders empty. Single-tenant deployments never hit this; - the wall is inert there. +Some system objects are **env-global** — **platform admin** sees all, members +none — e.g. identity tables a plugin writes via its own adapter +(`sys_sso_provider`, OAuth clients): + +- Reads of a tenant object pass the **Layer 0 tenant wall** (ADR-0095 D1): + `organization_id == `, AND-composed ahead of all + business RLS. A row with **null or absent** `organization_id` + (adapter-written rows often lack it) is **denied** — the list renders empty. + Under `single` the wall is inert. - The `viewAllRecords` superuser bit is **posture-gated and wall-blind**: it short-circuits **business RLS only**, and only on objects whose posture allows it (`access.default: 'private'`, `tenancy: { enabled: false }`, or a