Skip to content

Commit 4659ec2

Browse files
committed
feat(cloud): introduce cloud mode for ObjectStack with serverless deployment
- Added cloud-specific configuration and entry points for multi-project support. - Implemented Vercel serverless function as the API entry point. - Created templates for project provisioning (blank, CRM, todo). - Established a build process for Vercel deployment, including bundling and artifact management. - Updated package dependencies and lockfile to support new cloud features. - Added TypeScript definitions and configuration for cloud-specific modules.
1 parent 5eac718 commit 4659ec2

22 files changed

Lines changed: 979 additions & 10 deletions

apps/cloud/README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# @objectstack/cloud
2+
3+
Cloud-mode host for ObjectStack — multi-project, control-plane connected,
4+
deployed as a Vercel serverless function.
5+
6+
This app is the cloud counterpart to [`@objectstack/server`](../server),
7+
which hosts local single-project / standalone deployments.
8+
9+
## Modes
10+
11+
This config is **cloud-only**. Boot orchestration lives in
12+
`@objectstack/service-cloud`; this package only supplies the
13+
cloud-specific knobs:
14+
15+
- **`templates`** — Studio's template registry (Blank / CRM / Todo).
16+
- **`appBundles`** — filesystem-backed app bundle resolver.
17+
18+
Set `OBJECTSTACK_MODE=cloud` (default for this app) to boot the
19+
multi-project plugin stack.
20+
21+
## Local development
22+
23+
```bash
24+
# From repo root
25+
pnpm install
26+
pnpm --filter @objectstack/cloud dev
27+
```
28+
29+
## Build
30+
31+
```bash
32+
pnpm --filter @objectstack/cloud build
33+
```
34+
35+
Produces `dist/objectstack.config.js`, consumed by
36+
`objectstack serve --prebuilt`.
37+
38+
## Vercel deployment
39+
40+
`vercel.json` and `scripts/build-vercel.sh` mirror the apps/server
41+
deployment recipe — bundle `server/index.ts` with esbuild, copy Studio +
42+
Account SPAs into `public/`, and ship `api/[[...route]].js` as the
43+
catch-all serverless function.

apps/cloud/api/[[...route]].js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Vercel Serverless Function — Catch-all API route.
2+
//
3+
// This file MUST be committed to the repository so Vercel can detect it
4+
// as a serverless function during the pre-build phase.
5+
//
6+
// It delegates to the esbuild bundle (`_handler.js`) generated by
7+
// `scripts/bundle-api.mjs` during the Vercel build step. A separate
8+
// bundle file is used (rather than overwriting this file) so that:
9+
// 1. Vercel always finds this committed entry point (no "File not found").
10+
// 2. Vercel does not TypeScript-compile a .ts stub that references
11+
// source files absent at runtime (no ERR_MODULE_NOT_FOUND).
12+
//
13+
// @see ../server/index.ts — the actual server entrypoint
14+
// @see ../scripts/bundle-api.mjs — the esbuild bundler
15+
16+
export { default, config } from './_handler.js';

apps/cloud/objectstack.config.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* ObjectStack Cloud — Host Configuration
5+
*
6+
* Booted by `objectstack dev` / `objectstack serve` (see `package.json`)
7+
* and by the Vercel serverless entrypoint (`server/index.ts`).
8+
*
9+
* ## Boot mode
10+
*
11+
* This config is **cloud-only** — multi-project, control-plane connected,
12+
* with the Studio template registry and filesystem-backed app bundle
13+
* resolver wired in. Local single-project / standalone modes live in
14+
* `apps/server`. All boot orchestration lives in
15+
* `@objectstack/service-cloud`; this file only supplies the
16+
* apps/cloud-specific knobs (templates, app bundle resolution).
17+
*/
18+
19+
import { createBootStack } from '@objectstack/service-cloud';
20+
import { createFsAppBundleResolver } from './server/fs-app-bundle-resolver.js';
21+
import { templateRegistry } from './server/templates/registry.js';
22+
23+
const config = await createBootStack({
24+
cloud: {
25+
templates: templateRegistry,
26+
appBundles: createFsAppBundleResolver(),
27+
},
28+
});
29+
30+
export default config;

apps/cloud/package.json

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "@objectstack/cloud",
3+
"version": "4.0.4",
4+
"license": "Apache-2.0",
5+
"type": "module",
6+
"private": true,
7+
"scripts": {
8+
"dev": "objectstack dev",
9+
"build": "tsup",
10+
"start": "objectstack serve dist/objectstack.config.js --prebuilt",
11+
"doctor": "objectstack doctor",
12+
"typecheck": "tsc --noEmit",
13+
"test": "objectstack test",
14+
"clean": "rm -rf dist node_modules"
15+
},
16+
"dependencies": {
17+
"@hono/node-server": "^1.19.14",
18+
"@libsql/client": "^0.17.2",
19+
"@objectstack/driver-memory": "workspace:*",
20+
"@objectstack/driver-sql": "workspace:*",
21+
"@objectstack/driver-turso": "workspace:*",
22+
"@objectstack/hono": "workspace:*",
23+
"@objectstack/metadata": "workspace:*",
24+
"@objectstack/objectql": "workspace:*",
25+
"@objectstack/plugin-audit": "workspace:*",
26+
"@objectstack/plugin-auth": "workspace:*",
27+
"@objectstack/plugin-hono-server": "workspace:*",
28+
"@objectstack/plugin-security": "workspace:*",
29+
"@objectstack/plugin-setup": "workspace:*",
30+
"@objectstack/runtime": "workspace:*",
31+
"@objectstack/service-ai": "workspace:*",
32+
"@objectstack/service-analytics": "workspace:*",
33+
"@objectstack/service-automation": "workspace:*",
34+
"@objectstack/service-feed": "workspace:*",
35+
"@objectstack/service-cloud": "workspace:*",
36+
"@objectstack/service-package": "workspace:*",
37+
"@objectstack/service-tenant": "workspace:*",
38+
"@objectstack/spec": "workspace:*",
39+
"hono": "^4.12.14"
40+
},
41+
"devDependencies": {
42+
"@objectstack/cli": "workspace:*",
43+
"esbuild": "^0.28.0",
44+
"ts-node": "^10.9.2",
45+
"tsup": "^8.0.0",
46+
"tsx": "^4.21.0",
47+
"typescript": "^6.0.3"
48+
}
49+
}

apps/cloud/scripts/build-vercel.sh

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# Build script for Vercel deployment of @objectstack/server.
5+
#
6+
# Follows the Vercel deployment pattern:
7+
# - api/[[...route]].js is committed to the repo (Vercel detects it pre-build)
8+
# - esbuild bundles server/index.ts → api/_handler.js (self-contained bundle)
9+
# - The committed .js wrapper re-exports from _handler.js at runtime
10+
# - Studio SPA is built and copied to public/ for serving the UI
11+
# - External dependencies installed in api/node_modules/ (no symlinks)
12+
#
13+
# Steps:
14+
# 1. Build the project with turbo (includes studio)
15+
# 2. Bundle the API serverless function (→ api/_handler.js)
16+
# 3. Copy studio dist files to public/ for UI serving
17+
# 4. Install external deps in api/node_modules/ (resolve pnpm symlinks)
18+
19+
echo "[build-vercel] Starting server build..."
20+
21+
# 1. Build the project with turbo (from monorepo root)
22+
# This builds server, studio, and the account portal.
23+
cd ../..
24+
pnpm turbo run build --filter=@objectstack/server --filter=@objectstack/studio --filter=@objectstack/account
25+
cd apps/server
26+
27+
# 1b. Compile objectstack.config.ts → dist/objectstack.json (the metadata artifact).
28+
# MetadataPlugin reads this file at startup in local mode. Without it the kernel
29+
# cannot boot ("Cannot read artifact file … ENOENT").
30+
echo "[build-vercel] Compiling objectstack artifact..."
31+
pnpm objectstack build
32+
echo "[build-vercel] ✓ dist/objectstack.json generated"
33+
34+
# 2. Bundle API serverless function
35+
node scripts/bundle-api.mjs
36+
37+
# 2b. Copy the artifact into api/dist/ so Vercel includes it in the function
38+
# deployment package. The function runs with CWD=/var/task and resolves the
39+
# artifact relative to its own directory (api/), so api/dist/objectstack.json
40+
# maps to /var/task/apps/server/api/dist/objectstack.json at runtime.
41+
echo "[build-vercel] Copying artifact into api/dist/..."
42+
mkdir -p api/dist
43+
cp dist/objectstack.json api/dist/objectstack.json
44+
echo "[build-vercel] ✓ api/dist/objectstack.json ready"
45+
46+
# 3. Copy studio dist files to public/_studio/ for UI serving.
47+
# Studio is always mounted under /_studio/ (same convention as the CLI
48+
# static plugin). Vite builds with base: '/_studio/' so its asset URLs
49+
# and router basepath are already correct for this mount point.
50+
echo "[build-vercel] Copying studio dist to public/_studio/..."
51+
rm -rf public
52+
mkdir -p public/_studio
53+
if [ -d "../studio/dist" ]; then
54+
cp -r ../studio/dist/. public/_studio/
55+
echo "[build-vercel] ✓ Copied studio dist to public/_studio/"
56+
else
57+
echo "[build-vercel] ⚠ Studio dist not found (skipped)"
58+
fi
59+
60+
# 3b. Copy the account portal dist to public/_account/ — same pattern as
61+
# studio. The account SPA is always built with base: '/_account/'.
62+
echo "[build-vercel] Copying account dist to public/_account/..."
63+
mkdir -p public/_account
64+
if [ -d "../account/dist" ]; then
65+
cp -r ../account/dist/. public/_account/
66+
echo "[build-vercel] ✓ Copied account dist to public/_account/"
67+
else
68+
echo "[build-vercel] ⚠ Account dist not found (skipped)"
69+
fi
70+
71+
# 4. Install external dependencies in api/node_modules/ (no symlinks)
72+
# pnpm uses symlinks in node_modules/, which Vercel's serverless function
73+
# packaging cannot handle ("invalid deployment package" error).
74+
# We use npm to install external packages as real files next to the handler.
75+
echo "[build-vercel] Installing external dependencies for serverless function..."
76+
cat > api/_package.json << 'DEPS'
77+
{
78+
"private": true,
79+
"dependencies": {
80+
"@libsql/client": "0.14.0",
81+
"pino": "10.3.1",
82+
"pino-pretty": "13.1.3"
83+
}
84+
}
85+
DEPS
86+
cd api
87+
mv _package.json package.json
88+
npm install --production --no-package-lock --ignore-scripts --loglevel error
89+
rm package.json
90+
cd ..
91+
echo "[build-vercel] ✓ External dependencies installed in api/node_modules/"
92+
93+
echo "[build-vercel] Done. Static files in public/, serverless function in api/[[...route]].js → api/_handler.js"

apps/cloud/scripts/bundle-api.mjs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* Pre-bundles the Vercel serverless API function.
3+
*
4+
* Vercel's @vercel/node builder resolves pnpm workspace packages via symlinks,
5+
* which can cause esbuild to resolve to TypeScript source files rather than
6+
* compiled dist output — producing ERR_MODULE_NOT_FOUND at runtime.
7+
*
8+
* This script bundles server/index.ts with ALL dependencies inlined (including
9+
* npm packages), so the deployed function is self-contained. Only packages
10+
* with native bindings are kept external.
11+
*
12+
* Run from the apps/server directory during the Vercel build step.
13+
*/
14+
15+
import { build } from 'esbuild';
16+
17+
// Packages that cannot be bundled (native bindings / optional drivers)
18+
const EXTERNAL = [
19+
// Optional knex database drivers — never used at runtime, but knex requires() them
20+
'pg',
21+
'pg-native',
22+
'pg-query-stream',
23+
'mysql',
24+
'mysql2',
25+
'sqlite3',
26+
'oracledb',
27+
'tedious',
28+
// macOS-only native file watcher
29+
'fsevents',
30+
// LibSQL client — has native bindings, must remain external for Vercel
31+
'@libsql/client',
32+
// Logging libraries - use dynamic require, must be external
33+
'pino',
34+
'pino-pretty',
35+
];
36+
37+
await build({
38+
entryPoints: ['server/index.ts'],
39+
bundle: true,
40+
platform: 'node',
41+
format: 'esm',
42+
target: 'es2022',
43+
outfile: 'api/_handler.js',
44+
sourcemap: true,
45+
external: EXTERNAL,
46+
// Silence warnings about optional/unused require() calls in knex drivers
47+
logOverride: { 'require-resolve-not-external': 'silent' },
48+
// Vercel resolves ESM .js files correctly when "type": "module" is set.
49+
// CJS format would conflict with the project's "type": "module" setting,
50+
// causing Node.js to fail parsing require()/module.exports as ESM syntax.
51+
//
52+
// The createRequire banner provides a real `require` function in the ESM
53+
// scope. esbuild's __require shim (generated for CJS→ESM conversion)
54+
// checks `typeof require !== "undefined"` and uses it when available,
55+
// which fixes "Dynamic require of <builtin> is not supported" errors
56+
// from CJS dependencies like knex/tarn that require() Node.js built-ins.
57+
banner: {
58+
js: [
59+
'// Bundled by esbuild — see scripts/bundle-api.mjs',
60+
'import { createRequire } from "module";',
61+
'const require = createRequire(import.meta.url);',
62+
].join('\n'),
63+
},
64+
});
65+
66+
console.log('[bundle-api] Bundled server/index.ts → api/_handler.js');
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Re-export shim. Implementation lives in `@objectstack/service-cloud`.
5+
*/
6+
export { createFsAppBundleResolver } from '@objectstack/service-cloud';

0 commit comments

Comments
 (0)