Skip to content

Commit e792840

Browse files
Copilothotlong
andcommitted
feat: add Vercel deployment configuration for ObjectOS demo
- Add vercel.json with build, output, and routing config - Create api/index.ts serverless function bootstrapping ObjectStack kernel - Support conditional base path in vite.config.ts (/ for Vercel, /console/ for local) - Add hono, @hono/node-server, @objectstack/runtime as direct dependencies - Dynamic basename in BrowserRouter from import.meta.env.BASE_URL - Add .vercel/ to .gitignore Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 454b73e commit e792840

8 files changed

Lines changed: 212 additions & 5 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ package-lock.json
1818
# Turbo
1919
.turbo
2020

21+
# Vercel
22+
.vercel
23+

api/index.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Vercel Serverless Function — ObjectOS API
3+
*
4+
* Bootstraps the ObjectStack kernel with all ObjectOS plugins
5+
* and exposes the Hono app as a Vercel Node.js serverless function.
6+
*
7+
* The kernel is initialized once on cold-start and reused for
8+
* subsequent warm invocations.
9+
*/
10+
import { Hono } from 'hono';
11+
import { cors } from 'hono/cors';
12+
import { handle } from '@hono/node-server/vercel';
13+
import type { Plugin, PluginContext } from '@objectstack/runtime';
14+
15+
/* ------------------------------------------------------------------ */
16+
/* Hono App — created at module level so plugins can register routes */
17+
/* ------------------------------------------------------------------ */
18+
const app = new Hono();
19+
20+
// Global CORS
21+
app.use(
22+
'/api/v1/*',
23+
cors({
24+
origin: (origin) => origin ?? '*',
25+
credentials: true,
26+
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
27+
allowHeaders: ['Content-Type', 'Authorization'],
28+
}),
29+
);
30+
31+
// Health-check (always available, even before kernel boots)
32+
app.get('/api/v1/health', (c) =>
33+
c.json({
34+
status: 'ok',
35+
version: '0.1.0',
36+
environment: 'vercel',
37+
timestamp: new Date().toISOString(),
38+
}),
39+
);
40+
41+
/* ------------------------------------------------------------------ */
42+
/* Kernel bootstrap (runs once per cold-start) */
43+
/* ------------------------------------------------------------------ */
44+
let bootstrapPromise: Promise<void> | null = null;
45+
46+
/**
47+
* Minimal plugin that exposes our Hono app as the `http.server` service.
48+
* ObjectOS plugins call `context.getService('http.server').getRawApp()`
49+
* to register their HTTP routes.
50+
*/
51+
function createHttpServicePlugin(honoApp: Hono): Plugin {
52+
return {
53+
name: 'vercel-http-service',
54+
version: '1.0.0',
55+
dependencies: [],
56+
async init(context: PluginContext) {
57+
context.registerService('http.server', {
58+
getRawApp: () => honoApp,
59+
app: honoApp,
60+
getPort: () => 0,
61+
get: () => {},
62+
post: () => {},
63+
put: () => {},
64+
delete: () => {},
65+
patch: () => {},
66+
use: () => {},
67+
listen: async () => {},
68+
close: async () => {},
69+
getRoutes: () => new Map(),
70+
getMiddlewares: () => [],
71+
});
72+
},
73+
async start() {},
74+
async destroy() {},
75+
} as unknown as Plugin;
76+
}
77+
78+
async function bootstrapKernel(): Promise<void> {
79+
try {
80+
const { ObjectKernel } = await import('@objectstack/runtime');
81+
const kernel = new ObjectKernel();
82+
83+
// 1. Register the HTTP service shim first so it is available for
84+
// all subsequent plugin init/start phases.
85+
kernel.use(createHttpServicePlugin(app));
86+
87+
// 2. Import and register ObjectOS plugins (same order as objectstack.config.ts)
88+
const [
89+
{ MetricsPlugin },
90+
{ CachePlugin },
91+
{ StoragePlugin },
92+
{ BetterAuthPlugin },
93+
{ PermissionsPlugin },
94+
{ AuditLogPlugin },
95+
{ WorkflowPlugin },
96+
{ AutomationPlugin },
97+
{ JobsPlugin },
98+
{ NotificationPlugin },
99+
{ I18nPlugin },
100+
] = await Promise.all([
101+
import('@objectos/metrics'),
102+
import('@objectos/cache'),
103+
import('@objectos/storage'),
104+
import('@objectos/auth'),
105+
import('@objectos/permissions'),
106+
import('@objectos/audit'),
107+
import('@objectos/workflow'),
108+
import('@objectos/automation'),
109+
import('@objectos/jobs'),
110+
import('@objectos/notification'),
111+
import('@objectos/i18n'),
112+
]);
113+
114+
// Foundation
115+
kernel.use(new MetricsPlugin());
116+
kernel.use(new CachePlugin());
117+
kernel.use(new StoragePlugin());
118+
119+
// Core
120+
kernel.use(new BetterAuthPlugin());
121+
kernel.use(new PermissionsPlugin());
122+
kernel.use(new AuditLogPlugin());
123+
124+
// Logic
125+
kernel.use(new WorkflowPlugin());
126+
kernel.use(new AutomationPlugin());
127+
kernel.use(new JobsPlugin());
128+
129+
// Services
130+
kernel.use(new NotificationPlugin());
131+
kernel.use(new I18nPlugin());
132+
133+
await kernel.bootstrap();
134+
console.log('[ObjectOS] Kernel bootstrapped on Vercel');
135+
} catch (error) {
136+
console.error('[ObjectOS] Kernel bootstrap failed:', error);
137+
138+
// Register a catch-all error route so callers get a meaningful response
139+
app.all('/api/v1/*', (c) =>
140+
c.json(
141+
{
142+
success: false,
143+
error: 'Kernel bootstrap failed',
144+
message: error instanceof Error ? error.message : 'Unknown error',
145+
},
146+
500,
147+
),
148+
);
149+
}
150+
}
151+
152+
// Middleware: ensure kernel is ready before handling requests
153+
app.use('/api/v1/*', async (_c, next) => {
154+
if (!bootstrapPromise) {
155+
bootstrapPromise = bootstrapKernel();
156+
}
157+
await bootstrapPromise;
158+
await next();
159+
});
160+
161+
/* ------------------------------------------------------------------ */
162+
/* Export Vercel handler */
163+
/* ------------------------------------------------------------------ */
164+
export default handle(app);

apps/web/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/console/favicon.svg" />
5+
<link rel="icon" type="image/svg+xml" href="%BASE_URL%favicon.svg" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
77
<title>ObjectOS Console</title>
88
</head>

apps/web/src/main.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const queryClient = new QueryClient({
1717
createRoot(document.getElementById('root')!).render(
1818
<StrictMode>
1919
<QueryClientProvider client={queryClient}>
20-
<BrowserRouter basename="/console">
20+
<BrowserRouter basename={import.meta.env.BASE_URL.replace(/\/+$/, '')}>
2121
<App />
2222
</BrowserRouter>
2323
</QueryClientProvider>

apps/web/vite.config.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
import { defineConfig } from 'vite';
1+
import { defineConfig, type Plugin } from 'vite';
22
import react from '@vitejs/plugin-react';
33
import tailwindcss from '@tailwindcss/vite';
44
import { resolve } from 'path';
55

6+
const base = process.env.VERCEL ? '/' : '/console/';
7+
8+
/** Replace %BASE_URL% placeholders inside index.html at build time. */
9+
function htmlBaseUrl(): Plugin {
10+
return {
11+
name: 'html-base-url',
12+
transformIndexHtml(html) {
13+
return html.replace(/%BASE_URL%/g, base);
14+
},
15+
};
16+
}
17+
618
export default defineConfig({
7-
plugins: [react(), tailwindcss()],
8-
base: '/console/',
19+
plugins: [react(), tailwindcss(), htmlBaseUrl()],
20+
base,
921
resolve: {
1022
alias: {
1123
'@': resolve(__dirname, 'src'),

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
},
7272
"version": "0.1.0",
7373
"dependencies": {
74+
"@hono/node-server": "^1.19.0",
7475
"@objectos/audit": "workspace:*",
7576
"@objectos/auth": "workspace:*",
7677
"@objectos/automation": "workspace:*",
@@ -87,8 +88,10 @@
8788
"@objectql/driver-mongo": "^4.2.0",
8889
"@objectql/driver-sql": "^4.2.0",
8990
"@objectql/platform-node": "^4.2.0",
91+
"@objectstack/runtime": "1.1.0",
9092
"@objectstack/spec": "1.1.0",
9193
"build": "^0.1.4",
94+
"hono": "^4.11.0",
9295
"pino": "^10.3.0",
9396
"pino-pretty": "^13.1.3"
9497
}

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vercel.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"$schema": "https://openapi.vercel.sh/vercel.json",
3+
"buildCommand": "pnpm run build",
4+
"outputDirectory": "apps/web/dist",
5+
"installCommand": "pnpm install --no-frozen-lockfile",
6+
"framework": null,
7+
"functions": {
8+
"api/**/*.ts": {
9+
"maxDuration": 30
10+
}
11+
},
12+
"rewrites": [
13+
{ "source": "/api/v1/(.*)", "destination": "/api" },
14+
{ "source": "/((?!api/).*)", "destination": "/index.html" }
15+
]
16+
}

0 commit comments

Comments
 (0)