Skip to content

Commit d523c0d

Browse files
committed
feat(cloud): introduce cloud deployment with dual-mode bootstrap
- Added `apps/cloud` for control plane functionality, including project management and billing. - Implemented `KernelManager` to manage per-project kernels in cloud mode. - Created `DefaultProjectKernelFactory` to instantiate project-specific kernels with appropriate drivers and plugins. - Updated `apps/server` to support dual-mode operation: self-hosted and cloud. - Added comprehensive documentation on cloud vs self-hosted deployment strategies. - Introduced tests for `KernelManager` to ensure proper caching and eviction behavior.
1 parent 90aea6a commit d523c0d

9 files changed

Lines changed: 682 additions & 240 deletions

File tree

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"build": "objectstack compile",
1414
"typecheck": "tsc --noEmit",
1515
"test": "objectstack test",
16-
"test:e2e": "tsx test/e2e.test.ts",
16+
"test:e2e": "tsx test/multi-project-e2e.test.ts",
1717
"test:provisioning": "tsx test/provisioning.test.ts",
1818
"clean": "rm -rf dist node_modules"
1919
},

apps/server/server/bootstrap.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ import { MetadataPlugin } from '@objectstack/metadata';
4040
import { TursoDriver } from '@objectstack/driver-turso';
4141
import { SqlDriver } from '@objectstack/driver-sql';
4242
import 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';
4743
import { createControlPlanePlugins } from './control-plane-preset.js';
4844

4945
type IDataDriver = Contracts.IDataDriver;
@@ -88,6 +84,16 @@ function resolveShape(): ControlPlaneShape {
8884
async 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({

apps/server/test/e2e.test.ts

Lines changed: 0 additions & 174 deletions
This file was deleted.

0 commit comments

Comments
 (0)