@@ -40,10 +40,6 @@ import { MetadataPlugin } from '@objectstack/metadata';
4040import { TursoDriver } from '@objectstack/driver-turso' ;
4141import { SqlDriver } from '@objectstack/driver-sql' ;
4242import type { Contracts } from '@objectstack/spec' ;
43- import CrmApp from '../../../examples/app-crm/objectstack.config' ;
44- import TodoApp from '../../../examples/app-todo/objectstack.config' ;
45- import BiPluginManifest from '../../../examples/plugin-bi/objectstack.config' ;
46- import stackConfig from '../objectstack.config' ;
4743import { createControlPlanePlugins } from './control-plane-preset.js' ;
4844
4945type IDataDriver = Contracts . IDataDriver ;
@@ -88,6 +84,16 @@ function resolveShape(): ControlPlaneShape {
8884async function bootstrapSingle ( ) : Promise < BootstrapResult > {
8985 console . log ( '[Bootstrap] Shape: single' ) ;
9086 const kernel = new ObjectKernel ( ) ;
87+
88+ // stackConfig is imported dynamically so the multi-project shapes — which
89+ // never touch it — do not incur the Zod validation cost of the example
90+ // apps/plugins it references. A schema drift in one of the examples
91+ // shouldn't crash multi-project boots (or the E2E test harness) when
92+ // they don't need those bundles at all.
93+ const dyn = ( spec : string ) =>
94+ ( new Function ( 's' , 'return import(s)' ) as ( s : string ) => Promise < any > ) ( spec ) ;
95+ const stackConfig = ( await dyn ( '../objectstack.config.ts' ) ) . default ;
96+
9197 if ( ! stackConfig . plugins || stackConfig . plugins . length === 0 ) {
9298 throw new Error ( '[Bootstrap] No plugins found in stackConfig' ) ;
9399 }
@@ -188,20 +194,54 @@ async function bootstrapMultiProject(
188194 ) ,
189195 } ) ;
190196
191- // MVP: every project inherits the default bundle set. Swap for a
192- // registry-backed resolver once `sys_project_package` is consulted.
197+ // MVP app-bundle resolver.
198+ //
199+ // The example CRM / Todo / BI bundles are loaded lazily *and* gated on
200+ // an env flag so that:
201+ // 1. Test environments (E2E, unit tests) can skip them entirely —
202+ // the example `defineStack(...)` configs perform their own Zod
203+ // validation on import, so a single unrelated schema drift in
204+ // an example would otherwise crash bootstrap for everyone.
205+ // 2. Production multi-project deployments that do not ship the
206+ // reference apps (the typical case) avoid paying the cost.
207+ //
208+ // Set `OBJECTSTACK_BUNDLE_EXAMPLES=true` to get the legacy behaviour —
209+ // all three example bundles are attached to every project kernel.
210+ // Swap this resolver for a registry-backed one once
211+ // `sys_project_package` is consulted.
193212 const appBundles : AppBundleResolver = {
194213 async resolve ( ) {
195- return [ CrmApp , TodoApp , BiPluginManifest ] ;
214+ if ( process . env . OBJECTSTACK_BUNDLE_EXAMPLES !== 'true' ) {
215+ return [ ] ;
216+ }
217+ // Dynamic `new Function('return import(...)')(…)` sidesteps
218+ // TypeScript's static rootDir analysis — the example configs
219+ // live outside apps/server's tsconfig rootDir but are still
220+ // resolvable at runtime. Kept here intentionally so the tsc
221+ // typecheck doesn't need a dedicated include for examples.
222+ const dyn = ( spec : string ) =>
223+ ( new Function ( 's' , 'return import(s)' ) as ( s : string ) => Promise < any > ) ( spec ) ;
224+ const [ crm , todo , bi ] = await Promise . all ( [
225+ dyn ( '../../../examples/app-crm/objectstack.config.ts' ) ,
226+ dyn ( '../../../examples/app-todo/objectstack.config.ts' ) ,
227+ dyn ( '../../../examples/plugin-bi/objectstack.config.ts' ) ,
228+ ] ) ;
229+ return [ crm . default , todo . default , bi . default ] ;
196230 } ,
197231 } ;
198232
199233 // Per-project kernels only need the minimal base — driver is injected
200234 // by the factory. Additional service plugins (AI, automation, …) can
201235 // be added here when they are ready to run per-project.
202- const basePlugins : BasePluginsFactory = ( ) => [
203- new ObjectQLPlugin ( ) ,
204- new MetadataPlugin ( { watch : false } ) ,
236+ //
237+ // Both ObjectQL and Metadata are scoped to the project id via
238+ // `environmentId`. This keeps DatabaseLoader's baseFilter
239+ // (`env_id = <projectId>`) and protocol.saveMetaItem writes aligned,
240+ // so newly created objects are visible on the next read even though
241+ // every project already has its own physical database.
242+ const basePlugins : BasePluginsFactory = ( { projectId } ) => [
243+ new ObjectQLPlugin ( { environmentId : projectId } ) ,
244+ new MetadataPlugin ( { watch : false , environmentId : projectId } ) ,
205245 ] ;
206246
207247 const factory = new DefaultProjectKernelFactory ( {
0 commit comments