Skip to content

fix(platform-objects): organization_id optional so BU/Team create in single-tenant (ADR-0057) - #2178

Merged
xuyushun441-sys merged 1 commit into
mainfrom
fix/adr-0057-org-id-optional
Jun 22, 2026
Merged

fix(platform-objects): organization_id optional so BU/Team create in single-tenant (ADR-0057)#2178
xuyushun441-sys merged 1 commit into
mainfrom
fix/adr-0057-org-id-optional

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

You reported: creating a Business Unit in the Setup console errors.

Root cause

sys_business_unit.organization_id (and sys_team.organization_id) are required: true, but a single-tenant deployment has no sys_organization row and nothing auto-stamps one (OrgScopingPlugin is multi-tenant-only). So every create fails:

VALIDATION_FAILED: organization_id (required) → HTTP 400

This made the whole org-scoped identity graph uncreatable single-tenant — directly contradicting "BU is usable in the open/single-tenant edition" (ADR-0057 addendum).

Fix

Make organization_idoptional on sys_business_unit and sys_team:

  • single-tenantnull (consistent with "no org concept");
  • multi-tenant → still auto-stamped by OrgScopingPlugin; tenant_isolation RLS hides any null-org row (fail-closed) → no cross-tenant exposure.

Pure validation change (the columns are already nullable) — no migration.

sys_member / sys_invitation carry the same flag but are created only via better-auth org flows (which always supply an org) and aren't directly creatable in single-tenant Setup — left unchanged.

Verified (live os dev, single-tenant)

POST /data/sys_business_unit {name,kind} → 201 (organization_id: null)
POST /data/sys_team {name} → 201 (organization_id: null)

Before the fix both returned 400. Plus a dogfood regression test (boots single-tenant, creates both).

🤖 Generated with Claude Code

@vercel

vercelBot commented Jun 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJun 22, 2026 6:14am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/s labels Jun 22, 2026
@github-actions

github-actionsBot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/dogfood, @objectstack/platform-objects.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/packages.mdx(via @objectstack/platform-objects)
  • content/docs/concepts/setup-app.mdx(via @objectstack/platform-objects)
  • content/docs/guides/packages.mdx(via @objectstack/platform-objects)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…d optional (single-tenant create)
Single-tenant has no sys_organization row and no auto-stamp (org-scoping is multi-tenant-only), so a required organization_id made these objects uncreatable (VALIDATION_FAILED). Optional now: single-tenant = null; multi-tenant still auto-stamps via OrgScopingPlugin and tenant-isolation RLS hides null-org rows (fail-closed). +dogfood regression test. Verified live on os dev (BU + Team create 201 single-tenant).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xuyushun441-sys
xuyushun441-sysforce-pushed the fix/adr-0057-org-id-optional branch from b155607 to 27abccdCompareJune 22, 2026 06:11
@xuyushun441-sys
xuyushun441-sys merged commit 0df063e into mainJun 22, 2026
17 checks passed
@xuyushun441-sys
xuyushun441-sys deleted the fix/adr-0057-org-id-optional branch June 22, 2026 06:23
xuyushun441-sys added a commit that referenced this pull request Jun 22, 2026
…(ADR-0057) (#2182)
1) sys_member/sys_invitation organization_id -> optional (same class as #2178; single-tenant has no org/auto-stamp; multi-tenant still stamps + RLS hides null-org). 2) BusinessUnitGraphService.headOf() add missing orgScope() — it runs under SYSTEM_CTX so orgScope is the only isolation; without it headOf(buId) leaked a BU's manager_user_id across organizations (inconsistent with descendants()). +regression test (org1 service must not read an org2 BU's head).
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
xuyushun441-sys added a commit that referenced this pull request Jun 22, 2026
#2196)
The metadata→DB sync was additive-only (create table / add column), so any
non-additive metadata change — relax `required`, change type/length, drop or
rename a field — silently diverged from an existing database. The physical
column won at write time, surfacing a misleading `organization_id is required`
400 even though `/meta` reported the field optional.
Root-cause note: that 400 is NOT a validator bug. `record-validator` already
reads `required` from metadata and passes once the field is optional; the 400
is a DB NOT NULL violation that `rest-server` paraphrases as "is required".
So the fix is reconciling the physical schema + de-misleading the message,
not changing the validator's source of truth.
P1 — detection + warnings (schema-drift.ts, wired into SqlDriver.initObjects):
diff metadata vs physical columns, categorise safe / needs_confirm /
destructive (reusing SchemaDiffEntry). Boot warns once per divergence with an
actionable `os migrate` hint. rest-server's NOT NULL translation now carries a
drift-aware `hint` (VALIDATION_FAILED / fields envelope unchanged for back-compat).
P2 — dev auto-reconcile (SqlDriverConfig.autoMigrate:'safe', wired into
serve/dev in dev only): auto-applies the loosening subset (relax NOT NULL,
widen varchar) so an existing dev DB self-heals on restart. Never destructive;
force-disabled under NODE_ENV=production.
P3 — os migrate plan/apply (cli/commands/migrate/*): categorised dry-run +
reconcile, --allow-destructive gate, confirm prompt, --json. SQLite reconciles
via the official table-rebuild (copy→swap, data preserved); Postgres/MySQL
alter in place. Only examines objects in the loaded artifact; never auto-drops
a table absent from metadata.
Tests: driver-sql +12 (incl. #2178 repro self-heal with data preserved, prod
guard, destructive gating, PG/MySQL DDL-gen), cli integration (real
createStandaloneStack boot → detect → apply → in-sync), rest hint. Full build green.
Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…ite contract, storage.notNull is the column constraint (#4031)
The three adjudications on ADR-0113's open questions, and P0.
Q1 storage.notNull spelling: the write contract and the column constraint form a genuine 2x2 (required alone = the criteria_json posture; storage.notNull alone = the engine-populated column) — a single enum cannot express the storage-only cell. Q2 uniform semantics by EXPLICITIZATION: the field-required-notnull-explicit conversion stamps storage.notNull onto every pre-17 required field (writing down what the old text already meant, migration-chain-only — the loader never infers semantics from the physical column; the draft's load-time inference is rejected as self-contradictory). Q3 generalized: the non-regression invariant — a write may not take a record from compliant to violating; a pre-existing violation does not block writes that leave it in place — with required and requiredWhen as corollaries.
Implementation: FieldSchema.storage.{notNull} + parse-seam exclusivity vs requiredWhen (superRefine); record-validator rejects a PATCH nulling a required field (the update path previously skipped the required check entirely — a real hole closed); rule-validator applies the invariant to requiredWhen (the #3929 legacy-row lockout cured, its pinning test deliberately flipped); sql-driver createColumn + schema-drift read storage.notNull (a column stricter than its declaration is needs_confirm, silent when write-gated; dev auto-reconcile no longer strips a stray NOT NULL — supersedes the #2178 self-heal); conversion + chain step replayed by the composability gate; ADR-0113 -> Accepted; v17 notes + upgrade checklist; field ledger; major changesets.
spec 6903 / objectql 1179 / driver-sql 485 / wasm 126 green; all gates green.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/steststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@xuyushun441-sys@os-zhuang