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
13 changes: 13 additions & 0 deletions .changeset/fix-org-id-optional-single-tenant.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
---
"@objectstack/platform-objects": patch
---

Fix: `sys_business_unit` / `sys_team` could not be created in single-tenant deployments.

`organization_id` was `required`, but single-tenant has no `sys_organization` row and
nothing auto-stamps one (OrgScopingPlugin is multi-tenant-only), so every create failed
with `VALIDATION_FAILED: organization_id (required)`. Make `organization_id` optional on
both objects: single-tenant leaves it null; multi-tenant still auto-stamps it via
OrgScopingPlugin and tenant-isolation RLS hides any null-org row (fail-closed), so there is
no cross-tenant exposure. (sys_member / sys_invitation carry the same `required` flag but are
created only through better-auth org flows, which always supply an org — left unchanged.)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

/**
* ADR-0057 — org-scoped identity objects must be creatable in SINGLE-TENANT.
*
* Single-tenant deployments have no `sys_organization` row and no auto-stamp
* (OrgScopingPlugin is multi-tenant-only), so a `required` `organization_id`
* made sys_business_unit / sys_team uncreatable (VALIDATION_FAILED). The field
* is now optional; this proves the create path works with no org.
*/

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import showcaseStack from '@objectstack/example-showcase';
import { bootStack, type VerifyStack } from '@objectstack/verify';

describe('ADR-0057: org-scoped identity creatable single-tenant', () => {
let stack: VerifyStack;
let token: string;

beforeAll(async () => {
stack = await bootStack(showcaseStack, {}); // single-tenant: no org-scoping, no org row
token = await stack.signIn();
}, 120_000);

afterAll(async () => { await stack?.stop?.(); });

it('creates a sys_business_unit with no organization_id', async () => {
const res = await stack.apiAs(token, 'POST', '/data/sys_business_unit', { name: 'Engineering', kind: 'department' });
expect(res.status).toBe(201);
const body: any = await res.json();
expect(body.record?.organization_id ?? null).toBeNull();
});

it('creates a sys_team with no organization_id', async () => {
const res = await stack.apiAs(token, 'POST', '/data/sys_team', { name: 'Tiger Team' });
expect(res.status).toBe(201);
});
});
Original file line numberDiff line numberDiff line change
Expand Up@@ -114,8 +114,13 @@ export const SysBusinessUnit = ObjectSchema.create({

organization_id: Field.lookup('sys_organization', {
label: 'Organization',
required: true,
description: 'Tenant scope.',
// Optional: single-tenant deployments have no organization row (org-scoping
// is multi-tenant-only, nothing auto-stamps one) — requiring it would make
// the object uncreatable single-tenant. In multi-tenant, OrgScopingPlugin
// auto-stamps this from the active tenant and tenant-isolation RLS hides any
// null-org row (fail-closed). ADR-0057 addendum.
required: false,
description: 'Tenant scope. Null in single-tenant; auto-stamped in multi-tenant.',
group: 'Hierarchy',
}),

Expand Down
9 changes: 7 additions & 2 deletions packages/platform-objects/src/identity/sys-team.object.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -122,8 +122,13 @@ export const SysTeam = ObjectSchema.create({

organization_id: Field.lookup('sys_organization', {
label: 'Organization',
required: true,
description: 'Parent organization for this team',
// Optional: single-tenant deployments have no organization row (org-scoping
// is multi-tenant-only, nothing auto-stamps one) — requiring it would make
// the object uncreatable single-tenant. In multi-tenant, OrgScopingPlugin
// auto-stamps this from the active tenant and tenant-isolation RLS hides any
// null-org row (fail-closed). ADR-0057 addendum.
required: false,
description: 'Parent organization for this team. Null in single-tenant; auto-stamped in multi-tenant.',
group: 'Identity',
}),

Expand Down