88 * ## Boot modes
99 *
1010 * Selected via the `OBJECTSTACK_MODE` environment variable:
11- * - `standalone` (default) — single-project, no control plane
12- * - `cloud` — multi-project, control plane + per-project DBs
11+ *
12+ * - `project` (default) — local single-project deployment. Reuses the
13+ * cloud (multi-project) plugin stack but backs
14+ * it with two SQLite files (`control.db` for
15+ * the control plane, `proj_local.db` for the
16+ * single project's business data). Studio +
17+ * Auth fully operational. Aliases: `local`,
18+ * `single-project`.
19+ *
20+ * - `cloud` — multi-project, control-plane connected. See
21+ * @objectstack /service-cloud for details.
22+ * Alias: `multi-project`.
23+ *
24+ * - `standalone` — runtime-only (ObjectQL + REST + Driver). No
25+ * authentication, no Studio, no control plane.
26+ * For embedding in other frameworks or running
27+ * an internal-only data API.
1328 *
1429 * The legacy flag `OBJECTSTACK_MULTI_PROJECT=true` is still honoured as a
1530 * deprecated alias for `OBJECTSTACK_MODE=cloud` and will be removed in a
1631 * future major release.
1732 *
18- * ### Standalone mode (`OBJECTSTACK_MODE` unset or `standalone`)
19- *
20- * Single-project, offline-first. No control-plane DB is required.
21- * Authentication is fully real — first-run users are walked through `/setup`
22- * to create the owner account; thereafter every request requires a session.
23- * Required env vars:
24- * OBJECTSTACK_PROJECT_ID — project identity (e.g. "proj_local")
25- * OBJECTSTACK_DATABASE_URL — project business DB (file:./app.db, memory://mydb, libsql://…, https://…)
26- * OBJECTSTACK_DATABASE_AUTH_TOKEN — optional auth token for libSQL/Turso URLs
27- * OBJECTSTACK_DATABASE_DRIVER — driver name: sqlite | memory | turso (auto-detected from URL)
28- * OBJECTSTACK_ARTIFACT_PATH — path to compiled artifact (default: ./dist/objectstack.json)
29- * AUTH_SECRET — JWT signing secret (≥32 chars)
33+ * ### Common env vars
3034 *
31- * For Vercel / serverless deployments use a Turso database:
32- * TURSO_DATABASE_URL — libsql:// or https:// Turso URL (fallback alias for OBJECTSTACK_DATABASE_URL)
33- * TURSO_AUTH_TOKEN — Turso auth token (fallback alias for OBJECTSTACK_DATABASE_AUTH_TOKEN)
35+ * AUTH_SECRET — JWT signing secret (≥32 chars)
36+ * NEXT_PUBLIC_BASE_URL — public origin used by better-auth
37+ * OBJECTSTACK_PROJECT_ID — local project id (default: `proj_local`)
38+ * OBJECTSTACK_ARTIFACT_PATH — compiled artifact (default: ./dist/objectstack.json)
3439 *
35- * ### Cloud mode (`OBJECTSTACK_MODE=cloud`)
40+ * ### Project / Standalone DB
3641 *
37- * Multi-project, control-plane connected. See @objectstack/service-cloud for details.
38- * Required env vars:
39- * OBJECTSTACK_DATABASE_URL — control-plane DB URL
40- * OBJECTSTACK_DATABASE_AUTH_TOKEN — optional, for libSQL/Turso URLs
41- * AUTH_SECRET / NEXT_PUBLIC_BASE_URL — same as standalone
42+ * OBJECTSTACK_DATABASE_URL — overrides default file SQLite path
43+ * OBJECTSTACK_DATABASE_AUTH_TOKEN — auth token for libSQL/Turso URLs
44+ * OBJECTSTACK_DATABASE_DRIVER — driver name: sqlite | memory | turso
45+ * TURSO_DATABASE_URL / TURSO_AUTH_TOKEN — fallback aliases
4246 */
4347
4448import { resolve as resolvePath , dirname } from 'node:path' ;
4549import { fileURLToPath } from 'node:url' ;
50+ import { mkdirSync } from 'node:fs' ;
4651import { readFile } from 'node:fs/promises' ;
47- import { AppPlugin } from '@objectstack/runtime' ;
4852import { createCloudStack } from '@objectstack/service-cloud' ;
4953import { createSingleProjectPlugin } from './server/single-project-plugin.js' ;
5054import { templateRegistry } from './server/templates/registry.js' ;
@@ -54,16 +58,24 @@ function envFlag(name: string): boolean {
5458 return [ '1' , 'true' , 'yes' , 'on' ] . includes ( ( process . env [ name ] ?? '' ) . trim ( ) . toLowerCase ( ) ) ;
5559}
5660
61+ type Mode = 'project' | 'cloud' | 'standalone' ;
62+
5763/**
5864 * Resolve the deployment mode from environment.
65+ *
66+ * Default changed from `standalone` (legacy) to `project` (current). The
67+ * legacy `standalone` semantics — single-project local dev with full
68+ * Auth + Studio — moved under `project`. The new `standalone` value
69+ * means runtime-only (no Auth, no Studio).
5970 */
60- function resolveMode ( ) : 'standalone' | 'cloud' {
71+ function resolveMode ( ) : Mode {
6172 const raw = process . env . OBJECTSTACK_MODE ?. trim ( ) . toLowerCase ( ) ;
62- if ( raw === 'cloud' || raw === 'multi-project' /* legacy alias */ ) return 'cloud' ;
63- if ( raw === 'standalone' || raw === 'local' || raw === 'single-project' ) return 'standalone' ;
73+ if ( raw === 'cloud' || raw === 'multi-project' ) return 'cloud' ;
74+ if ( raw === 'standalone' ) return 'standalone' ;
75+ if ( raw === 'project' || raw === 'local' || raw === 'single-project' ) return 'project' ;
6476 if ( raw && raw . length > 0 ) {
6577 // eslint-disable-next-line no-console
66- console . warn ( `[objectstack] Unknown OBJECTSTACK_MODE=${ raw } ; falling back to "standalone ".` ) ;
78+ console . warn ( `[objectstack] Unknown OBJECTSTACK_MODE=${ raw } ; falling back to "project ".` ) ;
6779 }
6880 if ( envFlag ( 'OBJECTSTACK_MULTI_PROJECT' ) ) {
6981 // eslint-disable-next-line no-console
@@ -72,12 +84,10 @@ function resolveMode(): 'standalone' | 'cloud' {
7284 ) ;
7385 return 'cloud' ;
7486 }
75- return 'standalone ' ;
87+ return 'project ' ;
7688}
7789
78- // ── Boot mode ─────────────────────────────────────────────────────────────────
7990const mode = resolveMode ( ) ;
80- const isStandaloneMode = mode === 'standalone' ;
8191
8292const authSecret = process . env . AUTH_SECRET
8393 ?? 'dev-secret-please-change-in-production-min-32-chars' ;
@@ -90,33 +100,106 @@ const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
90100 : undefined )
91101 ?? `http://localhost:${ process . env . PORT ?? 3000 } ` ;
92102
93- // ── STANDALONE MODE ───────────────────────────────────────────────────────────
94-
103+ const serverDir = dirname ( fileURLToPath ( import . meta. url ) ) ;
95104const localProjectId = process . env . OBJECTSTACK_PROJECT_ID ?? 'proj_local' ;
96105const localArtifactPath = process . env . OBJECTSTACK_ARTIFACT_PATH
97- ?? resolvePath ( dirname ( fileURLToPath ( import . meta. url ) ) , 'dist/objectstack.json' ) ;
106+ ?? resolvePath ( serverDir , 'dist/objectstack.json' ) ;
107+
108+ // ── PROJECT MODE ─────────────────────────────────────────────────────────────
109+ //
110+ // Reuses `createCloudStack()` with two local SQLite files. The frontend
111+ // detects single-project mode via the `singleProject: true` payload from
112+ // `single-project-plugin`; everything else (auth, control plane,
113+ // kernel resolution, REST routing) follows the cloud code path.
114+
115+ async function createProjectStack ( ) {
116+ const dataDir = resolvePath ( serverDir , '.objectstack/data' ) ;
117+ mkdirSync ( dataDir , { recursive : true } ) ;
118+
119+ const controlDbUrl = `file:${ resolvePath ( dataDir , 'control.db' ) } ` ;
120+ const projectDbUrl = `file:${ resolvePath ( dataDir , 'proj_local.db' ) } ` ;
121+
122+ const stack = await createCloudStack ( {
123+ authSecret,
124+ baseUrl,
125+ controlDriverUrl : controlDbUrl ,
126+ appBundles : createFsAppBundleResolver ( ) ,
127+ // Project-mode per-project plugins. Identical to the cloud default
128+ // except for the AppPlugin tail — we eagerly load the local
129+ // artifact bundle (if present) so the single project picks up
130+ // schema definitions without needing a control-plane app row.
131+ basePlugins : async ( { projectId } : { projectId : string } ) => {
132+ const { ObjectQLPlugin } = await import ( '@objectstack/objectql' ) ;
133+ const { MetadataPlugin } = await import ( '@objectstack/metadata' ) ;
134+ const { createTenantPlugin } = await import ( '@objectstack/service-tenant' ) ;
135+ const { AuthPlugin } = await import ( '@objectstack/plugin-auth' ) ;
136+ const { SecurityPlugin } = await import ( '@objectstack/plugin-security' ) ;
137+ const { AuditPlugin } = await import ( '@objectstack/plugin-audit' ) ;
138+ const { AppPlugin } = await import ( '@objectstack/runtime' ) ;
98139
99- async function buildStandalonePlugins ( ) {
140+ let artifactBundle : any = null ;
141+ try {
142+ const raw = await readFile ( localArtifactPath , 'utf8' ) ;
143+ const parsed = JSON . parse ( raw ) ;
144+ artifactBundle = ( parsed ?. schemaVersion != null && parsed ?. metadata !== undefined )
145+ ? parsed . metadata
146+ : parsed ;
147+ } catch {
148+ // First boot before `objectstack build` — AppPlugin skipped.
149+ }
150+
151+ const plugins : any [ ] = [
152+ new ObjectQLPlugin ( { environmentId : projectId } ) ,
153+ new MetadataPlugin ( {
154+ watch : false ,
155+ environmentId : projectId ,
156+ artifactSource : { mode : 'local-file' , path : localArtifactPath } ,
157+ } ) ,
158+ createTenantPlugin ( { registerSystemObjects : true } ) ,
159+ new AuthPlugin ( { secret : authSecret , baseUrl } ) ,
160+ new SecurityPlugin ( ) ,
161+ new AuditPlugin ( ) ,
162+ ] ;
163+ if ( artifactBundle ) plugins . push ( new AppPlugin ( artifactBundle ) ) ;
164+ return plugins ;
165+ } ,
166+ } ) ;
167+
168+ // The cloud preset registers a `studio/runtime-config` route returning
169+ // `{ singleProject: false }`. Hono is first-match-wins, so we drop that
170+ // plugin and substitute our own which seeds local identity AND emits
171+ // `{ singleProject: true, … }`.
172+ const filtered = stack . plugins . filter (
173+ ( p : any ) => p ?. name !== 'com.objectstack.studio.runtime-config' ,
174+ ) ;
175+ filtered . push (
176+ createSingleProjectPlugin ( {
177+ projectId : localProjectId ,
178+ projectDatabaseUrl : projectDbUrl ,
179+ projectDatabaseDriver : 'sqlite' ,
180+ } ) ,
181+ ) ;
182+
183+ return {
184+ plugins : filtered ,
185+ api : stack . api ,
186+ } ;
187+ }
188+
189+ // ── STANDALONE MODE ──────────────────────────────────────────────────────────
190+ //
191+ // Runtime-only: ObjectQL + Driver + REST. No Auth, no Studio, no control
192+ // plane. Designed for embedding ObjectStack in other frameworks or
193+ // internal back-end services.
194+
195+ async function buildRuntimeOnlyConfig ( ) {
100196 const { ObjectQLPlugin } = await import ( '@objectstack/objectql' ) ;
101197 const { MetadataPlugin } = await import ( '@objectstack/metadata' ) ;
102- const { AuthPlugin } = await import ( '@objectstack/plugin-auth' ) ;
103- const { DriverPlugin } = await import ( '@objectstack/runtime' ) ;
104-
105- let artifactBundle : any = null ;
106- try {
107- const raw = await readFile ( localArtifactPath , 'utf8' ) ;
108- const parsed = JSON . parse ( raw ) ;
109- artifactBundle = ( parsed ?. schemaVersion && parsed ?. metadata !== undefined )
110- ? parsed . metadata
111- : parsed ;
112- } catch {
113- // Artifact not available yet (e.g. first run before compile) — AppPlugin skipped.
114- }
198+ const { DriverPlugin, AppPlugin } = await import ( '@objectstack/runtime' ) ;
115199
116- const serverDir = dirname ( fileURLToPath ( import . meta. url ) ) ;
117200 const dbUrl = process . env . OBJECTSTACK_DATABASE_URL ?. trim ( )
118201 || process . env . TURSO_DATABASE_URL ?. trim ( )
119- || `file:${ resolvePath ( serverDir , '.objectstack/data/app .db' ) } ` ;
202+ || `file:${ resolvePath ( serverDir , '.objectstack/data/standalone .db' ) } ` ;
120203 const dbAuthToken = process . env . OBJECTSTACK_DATABASE_AUTH_TOKEN ?. trim ( )
121204 || process . env . TURSO_AUTH_TOKEN ?. trim ( ) ;
122205 const dbDriver = process . env . OBJECTSTACK_DATABASE_DRIVER ?. trim ( )
@@ -128,17 +211,33 @@ async function buildStandalonePlugins() {
128211 driverPlugin = new DriverPlugin ( new MemoryDriver ( ) ) ;
129212 } else if ( dbDriver === 'turso' || / ^ ( l i b s q l | h t t p s ? ) : \/ \/ / i. test ( dbUrl ) ) {
130213 const { TursoDriver } = await import ( '@objectstack/driver-turso' ) ;
131- driverPlugin = new DriverPlugin ( new TursoDriver ( { url : dbUrl , authToken : dbAuthToken } ) as any ) ;
214+ driverPlugin = new DriverPlugin (
215+ new TursoDriver ( { url : dbUrl , authToken : dbAuthToken } ) as any ,
216+ ) ;
132217 } else {
133218 const { SqlDriver } = await import ( '@objectstack/driver-sql' ) ;
134219 const filename = dbUrl . replace ( / ^ f i l e : ( \/ \/ ) ? / , '' ) ;
135- const { mkdirSync } = await import ( 'node:fs' ) ;
136220 mkdirSync ( resolvePath ( filename , '..' ) , { recursive : true } ) ;
137221 driverPlugin = new DriverPlugin (
138- new SqlDriver ( { client : 'better-sqlite3' , connection : { filename } , useNullAsDefault : true } ) ,
222+ new SqlDriver ( {
223+ client : 'better-sqlite3' ,
224+ connection : { filename } ,
225+ useNullAsDefault : true ,
226+ } ) ,
139227 ) ;
140228 }
141229
230+ let artifactBundle : any = null ;
231+ try {
232+ const raw = await readFile ( localArtifactPath , 'utf8' ) ;
233+ const parsed = JSON . parse ( raw ) ;
234+ artifactBundle = ( parsed ?. schemaVersion != null && parsed ?. metadata !== undefined )
235+ ? parsed . metadata
236+ : parsed ;
237+ } catch {
238+ // No artifact yet — AppPlugin skipped.
239+ }
240+
142241 const plugins : any [ ] = [
143242 driverPlugin ,
144243 new MetadataPlugin ( {
@@ -147,32 +246,30 @@ async function buildStandalonePlugins() {
147246 artifactSource : { mode : 'local-file' , path : localArtifactPath } ,
148247 } ) ,
149248 new ObjectQLPlugin ( { environmentId : localProjectId } ) ,
150- new AuthPlugin ( { secret : authSecret , baseUrl, plugins : { organization : true , twoFactor : true , passkeys : false , magicLink : false , oidcProvider : true , deviceAuthorization : true } } ) ,
151- createSingleProjectPlugin ( { projectId : localProjectId } ) ,
152249 ] ;
250+ if ( artifactBundle ) plugins . push ( new AppPlugin ( artifactBundle ) ) ;
153251
154- if ( artifactBundle ) {
155- plugins . push ( new AppPlugin ( artifactBundle ) ) ;
156- }
157-
158- return plugins ;
159- }
160-
161- // ── Export ────────────────────────────────────────────────────────────────────
162-
163- const config = isStandaloneMode
164- ? {
165- plugins : await buildStandalonePlugins ( ) ,
252+ return {
253+ plugins,
166254 api : {
167255 enableProjectScoping : false ,
168256 projectResolution : 'none' as const ,
169257 } ,
170- }
171- : await createCloudStack ( {
172- authSecret,
173- baseUrl,
174- templates : templateRegistry ,
175- appBundles : createFsAppBundleResolver ( ) ,
176- } ) ;
258+ } ;
259+ }
260+
261+ // ── Export ──────────────────────────────────────────────────────────────────
262+
263+ const config =
264+ mode === 'cloud'
265+ ? await createCloudStack ( {
266+ authSecret,
267+ baseUrl,
268+ templates : templateRegistry ,
269+ appBundles : createFsAppBundleResolver ( ) ,
270+ } )
271+ : mode === 'standalone'
272+ ? await buildRuntimeOnlyConfig ( )
273+ : await createProjectStack ( ) ;
177274
178275export default config ;
0 commit comments