diff --git a/apps/server/.gitignore b/apps/server/.gitignore index 7b9ccd07e..82709e40f 100644 --- a/apps/server/.gitignore +++ b/apps/server/.gitignore @@ -1,6 +1,7 @@ # Build artifacts dist/ .turbo/ +public/ # Bundled API handler (generated during Vercel build) api/_handler.js diff --git a/apps/server/.npmrc b/apps/server/.npmrc new file mode 100644 index 000000000..ff33604d5 --- /dev/null +++ b/apps/server/.npmrc @@ -0,0 +1,3 @@ +# pnpm configuration for Vercel deployment +# Use hoisted node_modules instead of symlinks to avoid Vercel packaging issues +node-linker=hoisted diff --git a/apps/server/objectstack.config.ts b/apps/server/objectstack.config.ts index 0e483b00c..e91cd5a7c 100644 --- a/apps/server/objectstack.config.ts +++ b/apps/server/objectstack.config.ts @@ -4,6 +4,7 @@ import { defineStack } from '@objectstack/spec'; import { AppPlugin, DriverPlugin } from '@objectstack/runtime'; import { ObjectQLPlugin } from '@objectstack/objectql'; import { InMemoryDriver } from '@objectstack/driver-memory'; +import { TursoDriver } from '@objectstack/driver-turso'; import { AuthPlugin } from '@objectstack/plugin-auth'; import CrmApp from '../../examples/app-crm/objectstack.config'; import TodoApp from '../../examples/app-todo/objectstack.config'; @@ -31,13 +32,34 @@ export default defineStack({ description: 'Production server aggregating CRM, Todo and BI plugins', type: 'app', }, - + + // Datasource Mapping Configuration + // Routes different namespaces to different datasources for optimal performance + datasourceMapping: [ + // Example apps use in-memory driver for fast, ephemeral data + { namespace: 'crm', datasource: 'memory' }, + { namespace: 'todo', datasource: 'memory' }, + { namespace: 'bi', datasource: 'memory' }, + // System objects use Turso for persistent, production-grade storage + { namespace: 'sys', datasource: 'turso' }, + // Default fallback to memory driver + { default: true, datasource: 'memory' }, + ], + // Explicitly Load Plugins and Apps // The Runtime CLI will iterate this list and call kernel.use() plugins: [ new ObjectQLPlugin(), - // Register Default Driver (Memory) - new DriverPlugin(new InMemoryDriver()), + // Register Memory Driver for example apps (volatile, fast) + new DriverPlugin(new InMemoryDriver(), { name: 'memory' }), + // Register Turso Driver for system objects (persistent, production) + new DriverPlugin( + new TursoDriver({ + url: process.env.TURSO_DATABASE_URL ?? 'file:./data/server.db', + authToken: process.env.TURSO_AUTH_TOKEN, + }), + { name: 'turso' } + ), // Authentication — required for production (Vercel) deployments authPlugin, // Wrap Manifests/Stacks in AppPlugin adapter @@ -100,10 +122,26 @@ export const PreviewHostExample = defineStack({ type: 'app', }, + // Same datasource mapping as standard server + datasourceMapping: [ + { namespace: 'crm', datasource: 'memory' }, + { namespace: 'todo', datasource: 'memory' }, + { namespace: 'bi', datasource: 'memory' }, + { namespace: 'sys', datasource: 'turso' }, + { default: true, datasource: 'memory' }, + ], + // Same plugins as the standard host plugins: [ new ObjectQLPlugin(), - new DriverPlugin(new InMemoryDriver()), + new DriverPlugin(new InMemoryDriver(), { name: 'memory' }), + new DriverPlugin( + new TursoDriver({ + url: process.env.TURSO_DATABASE_URL ?? 'file:./data/server.db', + authToken: process.env.TURSO_AUTH_TOKEN, + }), + { name: 'turso' } + ), authPlugin, new AppPlugin(CrmApp), new AppPlugin(TodoApp), diff --git a/apps/server/scripts/build-vercel.sh b/apps/server/scripts/build-vercel.sh index 20b624f59..1aa8e9dc5 100755 --- a/apps/server/scripts/build-vercel.sh +++ b/apps/server/scripts/build-vercel.sh @@ -12,7 +12,8 @@ set -euo pipefail # Steps: # 1. Build the project with turbo (includes studio) # 2. Bundle the API serverless function (→ api/_handler.js) -# 3. Copy studio dist files to public/ for UI serving +# 3. Copy native/external modules into local node_modules for Vercel packaging +# 4. Copy studio dist files to public/ for UI serving echo "[build-vercel] Starting server build..." @@ -25,7 +26,43 @@ cd apps/server # 2. Bundle API serverless function node scripts/bundle-api.mjs -# 3. Copy studio dist files to public/ for UI serving +# 3. Copy native/external modules into local node_modules for Vercel packaging. +# +# This monorepo uses pnpm's default strict node_modules structure. External +# dependencies marked in bundle-api.mjs (@libsql/client) only exist in the +# monorepo root's node_modules/.pnpm/ virtual store. +# +# The vercel.json includeFiles pattern references node_modules/ relative to +# apps/server/, so we must copy the actual module files here for Vercel to +# include them in the serverless function's deployment package. +# +# Note: better-sqlite3 is NOT needed for Turso remote mode on Vercel. +echo "[build-vercel] Copying @libsql/client to local node_modules..." +src="../../node_modules/@libsql/client" +if [ -e "$src" ]; then + dest="node_modules/@libsql/client" + mkdir -p "$(dirname "$dest")" + cp -rL "$src" "$dest" + echo "[build-vercel] ✓ Copied @libsql/client" +else + echo "[build-vercel] ⚠ @libsql/client not found at $src (skipped)" +fi + +# Copy native binary subdirectories for @libsql/client +if [ -d "../../node_modules/@libsql" ]; then + mkdir -p "node_modules/@libsql" + for pkg in ../../node_modules/@libsql/*/; do + pkgname="$(basename "$pkg")" + if [ "$pkgname" != "client" ]; then # client already copied above + cp -rL "$pkg" "node_modules/@libsql/$pkgname" + echo "[build-vercel] ✓ Copied @libsql/$pkgname" + fi + done +else + echo "[build-vercel] ⚠ @libsql not found (skipped)" +fi + +# 4. Copy studio dist files to public/ for UI serving echo "[build-vercel] Copying studio dist to public/..." rm -rf public mkdir -p public diff --git a/apps/server/scripts/bundle-api.mjs b/apps/server/scripts/bundle-api.mjs index 57700c855..71e4bbbc3 100644 --- a/apps/server/scripts/bundle-api.mjs +++ b/apps/server/scripts/bundle-api.mjs @@ -14,8 +14,10 @@ import { build } from 'esbuild'; -// Packages that cannot be bundled (native bindings / optional drivers) +// Packages that cannot be bundled (native bindings / platform-specific modules) const EXTERNAL = [ + // @libsql/client has platform-specific native binaries that must be external + '@libsql/client', // Optional knex database drivers — never used at runtime, but knex requires() them 'pg', 'pg-native', @@ -23,6 +25,7 @@ const EXTERNAL = [ 'mysql', 'mysql2', 'sqlite3', + // better-sqlite3 NOT needed for Turso remote mode on Vercel 'oracledb', 'tedious', // macOS-only native file watcher diff --git a/apps/server/server/index.ts b/apps/server/server/index.ts index 9d080cfa9..88c4d4caa 100644 --- a/apps/server/server/index.ts +++ b/apps/server/server/index.ts @@ -13,6 +13,7 @@ import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime'; import { ObjectQLPlugin } from '@objectstack/objectql'; import { TursoDriver } from '@objectstack/driver-turso'; +import { InMemoryDriver } from '@objectstack/driver-memory'; import { createHonoApp } from '@objectstack/hono'; import { AuthPlugin } from '@objectstack/plugin-auth'; import { SecurityPlugin } from '@objectstack/plugin-security'; @@ -62,21 +63,40 @@ async function ensureKernel(): Promise { // Register ObjectQL engine await kernel.use(new ObjectQLPlugin()); - // Database driver - Turso (remote mode for Vercel) + // Register Memory Driver for example apps (volatile, fast) + await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')); + + // Register Turso Driver for system objects (persistent, production) const tursoUrl = process.env.TURSO_DATABASE_URL; const tursoToken = process.env.TURSO_AUTH_TOKEN; if (!tursoUrl || !tursoToken) { - throw new Error('Missing required environment variables: TURSO_DATABASE_URL and TURSO_AUTH_TOKEN'); + console.warn('[Vercel] Turso credentials not found, falling back to file-based SQLite'); } const tursoDriver = new TursoDriver({ - url: tursoUrl, + url: tursoUrl ?? 'file:./data/server.db', authToken: tursoToken, // Remote mode - no local sync needed for Vercel }); - await kernel.use(new DriverPlugin(tursoDriver)); + await kernel.use(new DriverPlugin(tursoDriver, 'turso')); + + // Configure datasource mapping + // This must be done before loading apps, so ObjectQL can route objects correctly + const ql = await kernel.getServiceAsync('ObjectQL'); + if (ql && typeof ql.setDatasourceMapping === 'function') { + ql.setDatasourceMapping([ + // Example apps use in-memory driver for fast, ephemeral data + { namespace: 'crm', datasource: 'memory' }, + { namespace: 'todo', datasource: 'memory' }, + { namespace: 'bi', datasource: 'memory' }, + // System objects use Turso for persistent, production-grade storage + { namespace: 'sys', datasource: 'turso' }, + // Default fallback to memory driver + { default: true, datasource: 'memory' }, + ]); + } // Load app manifests (BEFORE plugins that need object schemas) await kernel.use(new AppPlugin(CrmApp)); diff --git a/apps/server/vercel.json b/apps/server/vercel.json index fdaa07ceb..742e0c817 100644 --- a/apps/server/vercel.json +++ b/apps/server/vercel.json @@ -12,7 +12,8 @@ "functions": { "api/**/*.js": { "memory": 1024, - "maxDuration": 60 + "maxDuration": 60, + "includeFiles": "node_modules/@libsql/**" } }, "headers": [