Skip to content

Commit 7d350a4

Browse files
os-litantclaude
andauthored
docs(skills): state what single posture means for the organization count in objectstack-data (#17476)
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 <noreply@anthropic.com>
1 parent ea2940d commit 7d350a4

3 files changed

Lines changed: 42 additions & 36 deletions

File tree

‎skills/objectstack-data/SKILL.md‎

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -625,14 +625,17 @@ tenant-authored metadata.
625625

626626
## Seed Data & Fixtures (`defineSeed()`)
627627

628-
Object definition and its seed data live together — writing a `*.object.ts`
629-
almost always goes with a `*.seed.ts` (test fixtures, reference rows,
630-
bootstrap data). `defineSeed()` is type-safe: pass the object definition
631-
and TypeScript checks every record's field keys at compile time.
628+
Object definition and seed data live together — a `*.object.ts` usually
629+
pairs with a `*.seed.ts` (fixtures, reference rows, bootstrap data).
630+
`defineSeed()` is type-safe: TypeScript checks every record's field keys
631+
against the object definition.
632632

633-
> The factory is named `defineSeed` — **not** `defineDataset`. The `dataset`
634-
> name is reserved for the unrelated ADR-0021 analytics semantic layer
635-
> (`defineDataset` from `@objectstack/spec/ui`), which is not a seed factory.
633+
> The factory is `defineSeed` — **not** `defineDataset`, which is the
634+
> unrelated ADR-0021 analytics semantic layer (`@objectstack/spec/ui`).
635+
636+
> ⛔ `sys_organization` is platform-bootstrapped — never a seed target; the
637+
> deployment posture decides how many exist (`rules/security.md`
638+
> § Multi-tenancy).
636639
637640
### Quick start
638641

@@ -689,8 +692,8 @@ Full Zod shape: `node_modules/@objectstack/spec/src/data/seed.zod.ts`.
689692

690693
### `externalId` selection
691694

692-
Pick a stable natural business key. **Never use `id`** — UUIDs differ
693-
across environments.
695+
Pick a stable natural key. **Never use `id`** — UUIDs differ across
696+
environments.
694697

695698
| Scenario | Key |
696699
|:---------|:----|
@@ -705,11 +708,11 @@ For `lookup` fields, supply the **natural key** of the target record (not
705708
its UUID). The seed runner resolves at load time. Order seeds so parents
706709
appear before children in the exported array:
707710

708-
> If a lookup value matches no natural key, the loader now falls back to
709-
> resolving it as the target's `id` — so a reference to a real existing
710-
> record by internal id resolves instead of dangling to null. Natural keys
711-
> remain the portable default; rely on the id fallback only for records you
712-
> didn't seed (e.g. a system user).
711+
> If a lookup value matches no natural key, the loader falls back to
712+
> resolving it as the target's `id` — so a reference to an existing record
713+
> by internal id resolves instead of dangling to null. Natural keys remain
714+
> the portable default; rely on the id fallback only for records you didn't
715+
> seed (e.g. a system user).
713716
714717
```typescript
715718
const contacts = defineSeed(Contact, {

‎skills/objectstack-data/references/data-hooks.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -620,14 +620,16 @@ when a hook must work regardless of whether a user resolved.
620620
> **Two isolation axes — don't conflate them.** `organization_id` is
621621
> **org row-scoping**: many organizations share one database and every row
622622
> carries its owning org (`current_user.organizationId` filters reads/writes;
623-
> multi-org needs cloud + `@objectstack/organizations`). That is different from
623+
> it needs a walled posture — `OS_TENANCY_POSTURE=group|isolated` — with
624+
> `@objectstack/organizations` declared by the app, open core since
625+
> ADR-0132). That is different from
624626
> **environment / database-per-tenant** isolation (`service-tenant`,
625627
> `driver-turso`), where "tenant" means an entire environment/database and the
626628
> generic driver-layer `tenantId` knob can carry that environment id. The
627629
> object-metadata `tenancy.*` knob configures the *mechanism* (isolation on/off
628630
> + which column); the *value* you read and write is your `organization_id`
629-
> column. Community edition never populates an org, so `organizationId` is
630-
> `undefined` there.
631+
> column. Under `single` the bootstrapped Default Organization is the only
632+
> one, so there is nothing to scope by (`rules/security.md` § Multi-tenancy).
631633
632634
### `input` — Operation Parameters
633635

‎skills/objectstack-data/rules/security.md‎

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -152,33 +152,34 @@ fields: {
152152

153153
## Multi-tenancy
154154

155-
For SaaS, set `tenancy` on the object schema for row-level tenant isolation
156-
(the tenant field is injected on write and enforced on read). The block is
157-
**strict** — exactly two keys:
155+
Organization count is a **deployment posture** fact, never object metadata.
156+
`single` (default) = one logical tenant: the bootstrapped Default Organization
157+
only, a second refused (`403`); sub-units are business units. Tenants as
158+
`sys_organization` rows need a walled posture (`group`/`isolated`; open core,
159+
ADR-0132) — ⛔ never `single` + your own RLS. See ADR-0093, ADR-0105 §"Today's
160+
two postures", https://objectstack.ai/docs/deployment/tenancy-modes.
161+
162+
Within a posture, `tenancy` on the object is the row-level knob (stamped on
163+
write, enforced on read); **strict**, two keys:
158164

159165
```typescript
160166
tenancy: {
161-
enabled: true, // enable row-level tenant isolation
162-
// tenantField — NO default; omit it and the driver uses `organization_id`
167+
enabled: true,
168+
// tenantField: no default — omit it ⇒ `organization_id`
163169
}
164170
```
165171

166-
- **Database-per-tenant isolation is not object metadata** — it is an
167-
environment/deployment choice (each environment carries its own database URL).
168-
169172
## Platform-global / admin-only objects (visibility posture)
170173

171-
Some system/config objects are **env-global** (not partitioned per org) and
172-
should be visible to a **platform admin env-wide** but hidden from members —
173-
e.g. identity tables a plugin writes via its own adapter (`sys_sso_provider`,
174-
OAuth clients). These hit a non-obvious interaction:
175-
176-
- Reads of a tenant object pass the **Layer 0 tenant wall** (ADR-0095 D1): an
177-
`organization_id == <the caller's organization>` filter AND-composed ahead of
178-
every business RLS policy. Any row whose `organization_id` is **null or
179-
absent** (common for adapter-written rows that never get the tenant stamp) is
180-
**denied** — the list renders empty. Single-tenant deployments never hit this;
181-
the wall is inert there.
174+
Some system objects are **env-global** — **platform admin** sees all, members
175+
none — e.g. identity tables a plugin writes via its own adapter
176+
(`sys_sso_provider`, OAuth clients):
177+
178+
- Reads of a tenant object pass the **Layer 0 tenant wall** (ADR-0095 D1):
179+
`organization_id == <caller's organization>`, AND-composed ahead of all
180+
business RLS. A row with **null or absent** `organization_id`
181+
(adapter-written rows often lack it) is **denied** — the list renders empty.
182+
Under `single` the wall is inert.
182183
- The `viewAllRecords` superuser bit is **posture-gated and wall-blind**: it
183184
short-circuits **business RLS only**, and only on objects whose posture allows
184185
it (`access.default: 'private'`, `tenancy: { enabled: false }`, or a

0 commit comments

Comments
 (0)