11import { HttpApiSwagger } from "effect/unstable/httpapi" ;
22import { HttpEffect , HttpRouter } from "effect/unstable/http" ;
33import { Effect , Layer } from "effect" ;
4+ import { createClient } from "@libsql/client" ;
45
56import { composePluginApi , ExecutorApp , textFailureStrategy } from "@executor-js/api/server" ;
67
@@ -13,6 +14,7 @@ import { makeSelfHostSystemApiLayer } from "./system/handlers";
1314import { selfHostAccountMiddleware } from "./account" ;
1415import { loadConfig , SELF_HOST_NAMESPACE , SELF_HOST_SCHEMA_VERSION } from "./config" ;
1516import { createSelfHostDb , SelfHostDb , SelfHostDbProvider } from "./db/self-host-db" ;
17+ import { withSelfHostBootLock } from "./db/boot-lock" ;
1618import {
1719 SelfHostCodeExecutorProvider ,
1820 SelfHostHostConfig ,
@@ -49,29 +51,59 @@ export interface MakeSelfHostAppOptions {
4951}
5052
5153export const makeSelfHostApp = async ( options : MakeSelfHostAppOptions = { } ) => {
52- const config = loadConfig ( ) ;
53-
54- // ---- eager async boot: the shared libSQL handle -----------------------
55- const dbHandle = await createSelfHostDb ( {
56- path : options . dbPath ?? config . dbPath ,
57- namespace : SELF_HOST_NAMESPACE ,
58- version : SELF_HOST_SCHEMA_VERSION ,
54+ const databaseOverride = options . dbPath
55+ ? ( { kind : "file" , path : options . dbPath } as const )
56+ : undefined ;
57+ // An explicit test database is an isolation boundary: do not parse ambient
58+ // remote/stateless settings before replacing them with the throwaway file.
59+ const config = loadConfig ( {
60+ ... ( databaseOverride ? { database : databaseOverride , mcpMode : "stateful" } : { } ) ,
5961 } ) ;
62+ const database = config . database ;
63+
64+ const bootDatabase = async ( ) => {
65+ // ---- eager async boot: the shared libSQL handle ---------------------
66+ const dbHandle = await createSelfHostDb ( {
67+ database,
68+ namespace : SELF_HOST_NAMESPACE ,
69+ version : SELF_HOST_SCHEMA_VERSION ,
70+ } ) ;
6071
61- // Boot-time data migrations: each registry entry runs once and is stamped
62- // in the `data_migration` ledger; stamped entries are skipped without
63- // touching the data.
64- await Effect . runPromise ( runSqliteDataMigrations ( dbHandle . client , selfHostDataMigrations ) ) ;
72+ // Boot-time data migrations: each registry entry runs once and is stamped
73+ // in the `data_migration` ledger; stamped entries are skipped without
74+ // touching the data.
75+ await Effect . runPromise ( runSqliteDataMigrations ( dbHandle . client , selfHostDataMigrations ) ) ;
76+
77+ // ---- auth providers -------------------------------------------------
78+ // Better Auth: cookie/bearer/api-key identity + /api/auth handler + account
79+ // API + MCP OAuth seam, all over the shared libSQL handle.
80+ return { dbHandle, ...( await resolveAuthProviders ( dbHandle , config ) ) } ;
81+ } ;
6582
66- // ---- auth providers ---------------------------------------------------
67- // Better Auth: cookie/bearer/api-key identity + /api/auth handler + account
68- // API + MCP OAuth seam, all over the shared libSQL handle.
69- const { identityLayer, authHandler, betterAuth } = await resolveAuthProviders ( dbHandle ) ;
83+ // Vercel and similar hosts can cold-start multiple containers against one
84+ // remote database. Serialize executor schema creation, data migrations, and
85+ // first-run auth seeding with a renewable DB lease. The dedicated lock client
86+ // keeps heartbeats outside any migration transaction on the app's client.
87+ const remoteDatabase = database . kind === "remote" ? database : null ;
88+ const { dbHandle, identityLayer, authHandler, betterAuth } = remoteDatabase
89+ ? await ( async ( ) => {
90+ const lockClient = createClient ( {
91+ url : remoteDatabase . url ,
92+ ...( remoteDatabase . authToken ? { authToken : remoteDatabase . authToken } : { } ) ,
93+ } ) ;
94+ // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: the dedicated boot-lock connection must close on success and failure
95+ try {
96+ return await withSelfHostBootLock ( lockClient , bootDatabase ) ;
97+ } finally {
98+ lockClient . close ( ) ;
99+ }
100+ } ) ( )
101+ : await bootDatabase ( ) ;
70102
71103 // ---- the in-process MCP serving seams (+ shutdown hook) ----------------
72104 // Pass the pinned public origin so browser-approval URLs are reachable behind
73105 // a reverse proxy (not the internal 127.0.0.1 bind from the request URL).
74- const mcp = makeSelfHostMcpSeams ( dbHandle , betterAuth , config . webBaseUrl ) ;
106+ const mcp = makeSelfHostMcpSeams ( dbHandle , betterAuth , config . webBaseUrl , config . mcpMode ) ;
75107
76108 // CLI device-login discovery (`executor login`). Points the CLI at Better
77109 // Auth's device endpoints; `requestFormat: "json"` because those endpoints
@@ -99,7 +131,10 @@ export const makeSelfHostApp = async (options: MakeSelfHostAppOptions = {}) => {
99131 db : SelfHostDbProvider ,
100132 engine : { codeExecutor : SelfHostCodeExecutorProvider } , // decorator defaults to no-op (no metering)
101133 mcp : { auth : mcp . auth , sessions : mcp . sessions , reporter : mcp . reporter } ,
102- plugins : { provider : SelfHostPluginsProvider , config : SelfHostHostConfig } ,
134+ plugins : {
135+ provider : SelfHostPluginsProvider ,
136+ config : SelfHostHostConfig ,
137+ } ,
103138 errorCapture : ErrorCaptureLive ,
104139 } ,
105140 extensions : {
@@ -116,11 +151,21 @@ export const makeSelfHostApp = async (options: MakeSelfHostAppOptions = {}) => {
116151 // session-cookie-gated, delegating to the in-process MCP store.
117152 HttpRouter . add ( "*" , "/api/mcp-sessions/*" , HttpEffect . fromWebHandler ( mcp . approvalHandler ) ) ,
118153 // App-local admin (invite-code) API, served under /api/admin/*.
119- makeSelfHostAdminApiLayer ( { betterAuth, db : dbHandle , mountPrefix : "/api" } ) ,
154+ makeSelfHostAdminApiLayer ( {
155+ betterAuth,
156+ db : dbHandle ,
157+ mountPrefix : "/api" ,
158+ } ) ,
120159 // Public system API: /api/health + /api/setup-status (unauthenticated).
121- makeSelfHostSystemApiLayer ( { betterAuth, db : dbHandle , mountPrefix : "/api" } ) ,
160+ makeSelfHostSystemApiLayer ( {
161+ betterAuth,
162+ db : dbHandle ,
163+ mountPrefix : "/api" ,
164+ } ) ,
122165 // Swagger UI at /docs, over the /api-prefixed spec (matches the served paths).
123- HttpApiSwagger . layer ( composePluginApi ( selfHostPlugins ) . prefix ( "/api" ) , { path : "/docs" } ) ,
166+ HttpApiSwagger . layer ( composePluginApi ( selfHostPlugins ) . prefix ( "/api" ) , {
167+ path : "/docs" ,
168+ } ) ,
124169 ] ,
125170 } ,
126171 config : { mountPrefix : "/api" , failure : textFailureStrategy } ,
0 commit comments