Skip to content

Commit 795b6d1

Browse files
os-zhuangclaude
andauthored
refactor: single-source the multi-org (OS_MULTI_ORG_ENABLED) flag (#2350)
"Is this deployment multi-org?" was resolved in 10 sites across 8 packages with 3 subtly different inline expressions — including a bare process.env read in the SQL driver that skipped the OS_MULTI_TENANT deprecation warning every other site emits, so the driver could resolve the mode independently of the auth/security layer. Add resolveMultiOrgEnabled() to @objectstack/types (beside readEnvWithDeprecation) as the single source of truth and route all 10 sites through it: - driver-sql isMultiTenantMode() (+ new @objectstack/types dep) - plugin-auth /auth/config features + beforeCreateOrganization guard - objectql registry, runtime app-plugin, plugin-dev - cli serve (x2) + verify, cloud-connection Behaviour unchanged except the SQL driver now also emits the one-shot OS_MULTI_TENANT deprecation warning. Mirrors the resolveAuthzContext single-source pattern in @objectstack/core. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 73f4dc7 commit 795b6d1

12 files changed

Lines changed: 84 additions & 34 deletions

File tree

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
"@objectstack/types": patch
3+
---
4+
5+
refactor: single-source the multi-org (`OS_MULTI_ORG_ENABLED`) flag resolution
6+
7+
"Is this deployment multi-org?" was resolved in 10 places across 8 packages
8+
with three subtly different inline expressions:
9+
10+
- the canonical `String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED',
11+
'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false'` (objectql registry,
12+
plugin-dev, runtime app-plugin, cli serve/verify, cloud-connection),
13+
- a redundant `env.OS_MULTI_ORG_ENABLED !== undefined ? … : …` variant in
14+
plugin-auth (auth-manager `/auth/config` features + `beforeCreateOrganization`
15+
guard),
16+
- and a bare `process.env.OS_MULTI_ORG_ENABLED ?? process.env.OS_MULTI_TENANT`
17+
read in the SQL driver's `isMultiTenantMode()` — which skipped the
18+
`OS_MULTI_TENANT` deprecation warning every other site emits.
19+
20+
Because the SQL driver computed the mode independently of the auth/security
21+
layer, the driver's tenant-audit gate and the rest of the system could in
22+
principle disagree about whether tenant isolation is active.
23+
24+
Introduces `resolveMultiOrgEnabled()` in `@objectstack/types` (next to
25+
`readEnvWithDeprecation`, the natural leaf dependency) as the single source of
26+
truth, and routes all 10 sites through it. `@objectstack/driver-sql` gains a
27+
direct `@objectstack/types` dependency (previously it read `process.env`
28+
directly).
29+
30+
Behaviour is unchanged everywhere except the SQL driver, which now also emits
31+
the one-shot `OS_MULTI_TENANT`-is-deprecated warning — consistent with every
32+
other site. This mirrors the `resolveAuthzContext` single-source pattern in
33+
`@objectstack/core`. Follow-up (not in this change): a lint gate forbidding new
34+
inline reads of these env vars outside the helper.

packages/cli/src/commands/serve.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import chalk from 'chalk';
88
import { bundleRequire } from 'bundle-require';
99
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
1010
import { isHostConfig, shouldBootWithLibrary } from '../utils/plugin-detection.js';
11-
import { readEnvWithDeprecation } from '@objectstack/types';
11+
import { readEnvWithDeprecation, resolveMultiOrgEnabled } from '@objectstack/types';
1212
import { resolveObjectStackHome } from '@objectstack/runtime';
1313
import { LOG_LEVELS, resolveLogLevel, readLogLevelEnv } from '../utils/log-level.js';
1414
import {
@@ -1263,7 +1263,7 @@ export default class Serve extends Command {
12631263
// the `org-scoping` service at start() time and conditionally
12641264
// strips the wildcard `tenant_isolation` RLS when this plugin
12651265
// is absent — so registration order matters.
1266-
const multiTenant = String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false';
1266+
const multiTenant = resolveMultiOrgEnabled();
12671267
if (multiTenant) {
12681268
try {
12691269
const orgScopingPkg = '@objectstack/plugin-org-scoping';
@@ -2066,7 +2066,7 @@ export default class Serve extends Command {
20662066
consolePath: loadedPlugins.includes('ConsoleUI') ? CONSOLE_PATH : undefined,
20672067
driverLabel: resolvedDriverLabel,
20682068
databaseUrl: redactDbUrl(resolvedDatabaseUrl),
2069-
multiTenant: String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false',
2069+
multiTenant: resolveMultiOrgEnabled(),
20702070
seededAdmin,
20712071
});
20722072

packages/cli/src/commands/verify.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { Command, Flags } from '@oclif/core';
44
import chalk from 'chalk';
5-
import { readEnvWithDeprecation } from '@objectstack/types';
5+
import { resolveMultiOrgEnabled } from '@objectstack/types';
66
import {
77
bootStack,
88
runCrudVerification,
@@ -53,10 +53,7 @@ export default class Verify extends Command {
5353

5454
const { config, absolutePath } = await loadConfig(flags.app);
5555

56-
const multiTenant =
57-
flags['multi-tenant'] ||
58-
String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !==
59-
'false';
56+
const multiTenant = flags['multi-tenant'] || resolveMultiOrgEnabled();
6057

6158
// Data fidelity runs on its own pristine stack.
6259
let crud: VerifyReport;

packages/cloud-connection/src/marketplace-install-local-plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
*/
4343

4444
import type { Plugin, PluginContext } from '@objectstack/core';
45-
import { readEnvWithDeprecation } from '@objectstack/types';
45+
import { resolveMultiOrgEnabled } from '@objectstack/types';
4646
import { resolveCloudUrl } from './cloud-url.js';
4747
import { resolveMarketplacePublicBaseUrl } from './marketplace-public-url.js';
4848
import { LocalManifestSource, type InstalledManifestEntry } from './local-manifest-source.js';
@@ -789,7 +789,7 @@ export class MarketplaceInstallLocalPlugin implements Plugin {
789789
// writes tenant-scoped rows the same way AppPlugin's
790790
// single-tenant branch + SecurityPlugin's per-org replay do.
791791
if (opts.seedNow && datasets.length > 0) {
792-
const multiTenant = String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false';
792+
const multiTenant = resolveMultiOrgEnabled();
793793
try {
794794
const ql: any = ctx.getService('objectql');
795795
let metadata: any;

packages/objectql/src/registry.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { ServiceObject, ObjectSchema, ObjectOwnership } from '@objectstack/spec/data';
4-
import { readEnvWithDeprecation } from '@objectstack/types';
4+
import { resolveMultiOrgEnabled } from '@objectstack/types';
55
import { ObjectStackManifest, ManifestSchema, InstalledPackage, InstalledPackageSchema } from '@objectstack/spec/kernel';
66
import { AppSchema } from '@objectstack/spec/ui';
77
import { applyProtection } from '@objectstack/spec/shared';
@@ -420,8 +420,7 @@ export class SchemaRegistry {
420420
this.multiTenant = options.multiTenant;
421421
} else {
422422
// Mirror the SecurityPlugin / CLI banner default (env-driven, off by default).
423-
this.multiTenant =
424-
String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false';
423+
this.multiTenant = resolveMultiOrgEnabled();
425424
}
426425

427426
// ADR-0048 — default to a loud error on cross-package collision; allow an

packages/plugins/driver-sql/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"dependencies": {
2121
"@objectstack/core": "workspace:*",
2222
"@objectstack/spec": "workspace:*",
23+
"@objectstack/types": "workspace:*",
2324
"knex": "^3.2.10",
2425
"nanoid": "^5.1.15"
2526
},

packages/plugins/driver-sql/src/sql-driver.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { parseAutonumberFormat, renderAutonumber, missingFieldValues, type Auton
1212
import type { IDataDriver } from '@objectstack/spec/contracts';
1313
import { StorageNameMapping } from '@objectstack/spec/system';
1414
import { ExternalSchemaModeViolationError } from '@objectstack/spec/shared';
15+
import { resolveMultiOrgEnabled } from '@objectstack/types';
1516
import {
1617
diffManagedTable,
1718
driftKey,
@@ -2229,9 +2230,10 @@ export class SqlDriver implements IDataDriver {
22292230
private _multiTenantMode?: boolean;
22302231
protected isMultiTenantMode(): boolean {
22312232
if (this._multiTenantMode === undefined) {
2232-
const raw =
2233-
process.env.OS_MULTI_ORG_ENABLED ?? process.env.OS_MULTI_TENANT ?? 'false';
2234-
this._multiTenantMode = String(raw).toLowerCase() !== 'false';
2233+
// Single source of truth (shared with auth/registry/CLI) — previously
2234+
// this read `process.env` inline and so never emitted the
2235+
// `OS_MULTI_TENANT` deprecation warning the other sites do.
2236+
this._multiTenantMode = resolveMultiOrgEnabled();
22352237
}
22362238
return this._multiTenantMode;
22372239
}

packages/plugins/plugin-auth/src/auth-manager.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
} from '@objectstack/spec/system';
1313
import type { IDataEngine } from '@objectstack/core';
1414
import type { IEmailService } from '@objectstack/spec/contracts';
15-
import { readEnvWithDeprecation } from '@objectstack/types';
15+
import { readEnvWithDeprecation, resolveMultiOrgEnabled } from '@objectstack/types';
1616
import { mapMembershipRole, BUILTIN_ROLE_PLATFORM_ADMIN } from '@objectstack/spec';
1717
import { createObjectQLAdapterFactory, withSystemReadContext } from './objectql-adapter.js';
1818
import {
@@ -906,13 +906,7 @@ export class AuthManager {
906906
// 2. else `OS_MULTI_TENANT` (multi-tenant deployments are always
907907
// multi-org), default `'false'` → single-org / per-env runtime.
908908
beforeCreateOrganization: async () => {
909-
const env = (globalThis as any)?.process?.env ?? {};
910-
const explicit = env.OS_MULTI_ORG_ENABLED;
911-
const legacy = explicit === undefined
912-
? readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT')
913-
: explicit;
914-
const flag = String(legacy ?? 'false').toLowerCase();
915-
if (flag === 'false') {
909+
if (!resolveMultiOrgEnabled()) {
916910
const { APIError } = await import('better-auth/api');
917911
throw new APIError('FORBIDDEN', {
918912
message:
@@ -1601,11 +1595,7 @@ export class AuthManager {
16011595
// Resolution order: explicit `OS_MULTI_ORG_ENABLED` wins, else fall
16021596
// back to legacy `OS_MULTI_TENANT` (multi-tenant deployments are always
16031597
// multi-org); default `'false'` → single-org / per-env runtime.
1604-
const multiOrgEnv = (globalThis as any)?.process?.env ?? {};
1605-
const multiOrgRaw = multiOrgEnv.OS_MULTI_ORG_ENABLED !== undefined
1606-
? multiOrgEnv.OS_MULTI_ORG_ENABLED
1607-
: (readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false');
1608-
const multiOrgEnabled = String(multiOrgRaw).toLowerCase() !== 'false';
1598+
const multiOrgEnabled = resolveMultiOrgEnabled();
16091599

16101600
// Legal links shown beneath the login / register cards. Defaults to
16111601
// the public ObjectStack pages so vanilla deployments don't link to

packages/plugins/plugin-dev/src/dev-plugin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { Plugin, PluginContext, createMemoryCache, createMemoryQueue, createMemoryJob, createMemoryI18n } from '@objectstack/core';
4-
import { readEnvWithDeprecation } from '@objectstack/types';
4+
import { resolveMultiOrgEnabled } from '@objectstack/types';
55

66
/**
77
* All 17 core kernel service names as defined in CoreServiceName.
@@ -584,14 +584,14 @@ export class DevPlugin implements Plugin {
584584
// because SecurityPlugin.start() probes the `org-scoping` service and
585585
// caches the result for the lifetime of the plugin.
586586
if (enabled('security')) {
587-
const multiTenant = String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false';
587+
const multiTenant = resolveMultiOrgEnabled();
588588
if (multiTenant) {
589589
try {
590590
const { OrgScopingPlugin } = await import('@objectstack/plugin-org-scoping') as any;
591591
this.childPlugins.push(new OrgScopingPlugin());
592592
ctx.logger.info(' ✔ Org-scoping plugin enabled (multi-tenant: organization_id auto-stamp, per-org seed)');
593593
} catch {
594-
ctx.logger.warn(' ✘ OS_MULTI_TENANT=true but @objectstack/plugin-org-scoping not installed');
594+
ctx.logger.warn(' ✘ OS_MULTI_ORG_ENABLED=true but @objectstack/plugin-org-scoping not installed');
595595
}
596596
}
597597
try {

packages/runtime/src/app-plugin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
import { Plugin, PluginContext } from '@objectstack/core';
4-
import { readEnvWithDeprecation } from '@objectstack/types';
4+
import { resolveMultiOrgEnabled } from '@objectstack/types';
55
import { SeedLoaderService } from './seed-loader.js';
66
import { loadDisabledPackageIds } from './package-state-store.js';
77
import type { IMetadataService, II18nService } from '@objectstack/spec/contracts';
@@ -662,7 +662,7 @@ export class AppPlugin implements Plugin {
662662
// step. So we skip it. Single-tenant deployments keep the
663663
// legacy behaviour: seed immediately at boot so there's
664664
// always demo data without needing an org insert.
665-
const multiTenant = String(readEnvWithDeprecation('OS_MULTI_ORG_ENABLED', 'OS_MULTI_TENANT') ?? 'false').toLowerCase() !== 'false';
665+
const multiTenant = resolveMultiOrgEnabled();
666666
if (multiTenant) {
667667
ctx.logger.info('[Seeder] multi-tenant mode — skipping inline seed; per-org replay will run on sys_organization insert');
668668
} else {

0 commit comments

Comments
 (0)