Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/server/.gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Build artifacts
dist/
.turbo/
public/

# Bundled API handler (generated during Vercel build)
api/_handler.js
Expand Down
3 changes: 3 additions & 0 deletions apps/server/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# pnpm configuration for Vercel deployment
# Use hoisted node_modules instead of symlinks to avoid Vercel packaging issues
node-linker=hoisted
46 changes: 42 additions & 4 deletions apps/server/objectstack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
Expand Down
41 changes: 39 additions & 2 deletions apps/server/scripts/build-vercel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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..."

Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion apps/server/scripts/bundle-api.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@

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',
'pg-query-stream',
'mysql',
'mysql2',
'sqlite3',
// better-sqlite3 NOT needed for Turso remote mode on Vercel
'oracledb',
'tedious',
// macOS-only native file watcher
Expand Down
28 changes: 24 additions & 4 deletions apps/server/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -62,21 +63,40 @@ async function ensureKernel(): Promise<ObjectKernel> {
// 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));
Expand Down
3 changes: 2 additions & 1 deletion apps/server/vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"functions": {
"api/**/*.js": {
"memory": 1024,
"maxDuration": 60
"maxDuration": 60,
"includeFiles": "node_modules/@libsql/**"
}
},
"headers": [
Expand Down
Loading