| title | Deploy to Vercel |
|---|---|
| description | Deploy ObjectStack applications to Vercel — Server mode (recommended) for real API endpoints or MSW mode for static demos |
ObjectStack 11 deploys to Vercel in server mode — serverless functions running the Hono adapter (@objectstack/hono). The published ObjectStack Studio/console ships as a static Vite SPA that points at a separate ObjectStack server (via VITE_SERVER_URL).
| Mode | Runtime | Vercel Feature | Use Case |
|---|---|---|---|
| Server (default) | Node.js / Edge | Serverless Functions | Production apps, Studio, real database |
In MSW mode the entire ObjectStack kernel runs in the browser. Mock Service Worker intercepts fetch calls and routes them to an in-memory ObjectQL engine — no backend required.
┌─────────────────────────── Browser ───────────────────────────┐
│ React App → fetch('/api/v1/data/task') │
│ ↓ │
│ MSW Service Worker (intercepts request) │
│ ↓ │
│ ObjectKernel → ObjectQL → InMemoryDriver │
│ ↓ │
│ JSON Response → React App │
└───────────────────────────────────────────────────────────────┘
// objectstack.config.ts
import { defineStack } from '@objectstack/spec';
import * as objects from './src/objects';
export default defineStack({
manifest: {
id: 'com.example.myapp',
name: 'My App',
version: '1.0.0',
type: 'app',
},
objects: Object.values(objects),
data: [
{
object: 'task',
mode: 'upsert',
externalId: 'subject',
records: [
{ subject: 'Learn ObjectStack', status: 'in_progress', priority: 'high' },
],
},
],
});Create a kernel factory that boots the entire stack in the browser:
// src/mocks/createKernel.ts
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { MSWPlugin } from '@objectstack/plugin-msw';
export async function createKernel(appConfigs: any[]) {
const kernel = new ObjectKernel();
await kernel.use(new ObjectQLPlugin());
await kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory'));
for (const config of appConfigs) {
await kernel.use(new AppPlugin(config));
}
await kernel.use(new MSWPlugin({
enableBrowser: true,
baseUrl: '/api/v1',
logRequests: true,
}));
await kernel.bootstrap();
return kernel;
}// src/main.tsx
import appConfig from '../objectstack.config';
import { createKernel } from './mocks/createKernel';
async function bootstrap() {
// Boot the in-browser kernel before rendering
await createKernel([appConfig]);
// Now render — all fetch('/api/v1/...') calls are intercepted by MSW
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
}
bootstrap();MSW requires the Service Worker file in your public/ directory. Add the init script and configure Vite:
# Generate the MSW Service Worker file
npx msw init public --save// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
optimizeDeps: {
include: ['msw', 'msw/browser', '@objectstack/spec'],
},
});{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "vite",
"buildCommand": "vite build",
"outputDirectory": "dist",
"build": {
"env": {
"VITE_USE_MOCK_SERVER": "true"
}
},
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}If your project lives in a monorepo (e.g. pnpm workspaces + Turborepo), update the install and build commands:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "vite",
"installCommand": "cd ../.. && pnpm install",
"buildCommand": "cd ../.. && pnpm turbo run build --filter=@myorg/my-app",
"outputDirectory": "dist",
"build": {
"env": {
"VITE_USE_MOCK_SERVER": "true"
}
},
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}Set the Root Directory in Vercel project settings to the app's folder (e.g. apps/my-app).
In Server mode, ObjectStack runs inside Vercel Serverless Functions. API requests are handled by real backend logic with a real database.
┌─── Vercel ────────────────────────────────────────────────────┐
│ │
│ Static Assets (React SPA) │
│ ┌───────────────────────────────────┐ │
│ │ /index.html, /assets/* │ │
│ └───────────────────────────────────┘ │
│ │
│ Serverless Functions │
│ ┌───────────────────────────────────┐ │
│ │ /api/[...objectstack] │ │
│ │ → ObjectKernel │ │
│ │ → ObjectQL │ │
│ │ → PostgreSQL / MongoDB / ... │──── External DB │
│ └───────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────┘
This is a valid self-contained pattern when you want to ship a Vite SPA and its API from a single Vercel project: the SPA is served as static assets, and a Hono-based serverless function handles /api/* requests.
1. Create the kernel singleton (api/_kernel.ts — prefixed with _ to prevent Vercel from creating a route):
// api/_kernel.ts
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { createHonoApp } from '@objectstack/hono';
import { Hono } from 'hono';
import appConfig from '../objectstack.config';
let _kernel: ObjectKernel | null = null;
let _app: Hono | null = null;
async function ensureKernel(): Promise<ObjectKernel> {
if (_kernel) return _kernel;
_kernel = new ObjectKernel();
await _kernel.use(new ObjectQLPlugin());
await _kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory'));
await _kernel.use(new AppPlugin(appConfig));
await _kernel.bootstrap();
return _kernel;
}
export async function ensureApp(): Promise<Hono> {
if (_app) return _app;
const kernel = await ensureKernel();
_app = createHonoApp({ kernel, prefix: '/api/v1' });
return _app;
}2. Create the API entrypoint (api/index.ts):
// api/index.ts
import { Hono } from 'hono';
import { handle } from 'hono/vercel';
import { ensureApp } from './_kernel';
const app = new Hono();
app.all('*', async (c) => {
const inner = await ensureApp();
return inner.fetch(c.req.raw);
});
export default handle(app);3. vercel.json:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "vite",
"buildCommand": "vite build",
"outputDirectory": "dist",
"build": {
"env": {
"VITE_USE_MOCK_SERVER": "false",
"VITE_SERVER_URL": ""
}
},
"rewrites": [
{ "source": "/api/(.*)", "destination": "/api" },
{ "source": "/((?!api/).*)", "destination": "/index.html" }
]
}Configure these in Vercel Project Settings → Environment Variables:
| Variable | Description |
|---|---|
VITE_USE_MOCK_SERVER |
true = in-browser MSW kernel; false = real backend. A build-time flag baked into the SPA bundle. |
VITE_SERVER_URL |
Backend API URL (empty for same-origin) |
The Cloud control-plane distribution lives outside this framework repo. If you deploy a Cloud host on Vercel, remember that Vercel functions have an ephemeral, read-only filesystem — local SQLite and local-FS storage will not persist between invocations. Use a durable database and S3-compatible object storage for artifacts.
| Variable | Required | Description |
|---|---|---|
OS_AUTH_SECRET |
yes | ≥ 32 chars (AUTH_SECRET is accepted as a deprecated alias) |
OS_CONTROL_DATABASE_URL |
yes | libsql://… (Turso) — control plane DB |
OS_CONTROL_DATABASE_AUTH_TOKEN |
yes | Turso token (or TURSO_AUTH_TOKEN) |
OS_STORAGE_ADAPTER |
yes | s3 (must, on Vercel) |
OS_S3_BUCKET |
yes (s3) | Bucket name |
OS_S3_REGION |
yes (s3) | us-east-1, or auto for Cloudflare R2 |
OS_S3_ENDPOINT |
optional | For R2 / MinIO / B2 / non-AWS |
OS_S3_ACCESS_KEY_ID |
optional | Falls back to AWS SDK credential chain |
OS_S3_SECRET_ACCESS_KEY |
optional | Falls back to AWS SDK credential chain |
OS_S3_FORCE_PATH_STYLE |
optional | 1 for MinIO / self-hosted |
**Never commit secrets.** Use Vercel's environment variable UI or the Vercel CLI (`vercel env add`) to configure tokens and S3 credentials.See Publish, Versioning & Preview for the full storage matrix (native S3, Cloudflare R2, MinIO, etc.) and what the cloud control plane stores in each bucket.
The mode is selected at build time via the VITE_USE_MOCK_SERVER flag, which is baked into the SPA bundle:
VITE_USE_MOCK_SERVER=true→ the in-browser MSW kernel (static demo).VITE_USE_MOCK_SERVER=false→ the SPA talks to a real backend atVITE_SERVER_URL(empty = same-origin).
Because the flag is compiled into the bundle, there is no runtime ?mode= URL switch — to change modes you must rebuild with the desired value.
- [ ]
api/index.tsHono entrypoint exists withhandle(app)export - [ ]
api/_kernel.tsboots the kernel with the correct driver - [ ]
vercel.jsonsetsVITE_USE_MOCK_SERVER=falseandVITE_SERVER_URL=(empty) - [ ] Rewrite rule routes
/api/*to/apiand excludes/api/from SPA rewrite - [ ]
DATABASE_URLis configured in Vercel environment variables (for production drivers) - [ ] CORS is configured if frontend and API are on different origins
- [ ]
msw init public --savehas been run (Service Worker inpublic/) - [ ]
vercel.jsonspecifies"framework": "vite"and SPA rewrites - [ ]
VITE_USE_MOCK_SERVER=trueis set in build environment - [ ] Seed data is defined in
objectstack.config.ts(dataarray)
| Feature | MSW Mode | Server Mode |
|---|---|---|
| Database | In-memory (browser) | PostgreSQL, MongoDB, etc. |
| Data Persistence | Per session (lost on refresh) | Persistent |
| Cold Start | None (client-side) | ~200ms (Serverless) |
| Offline Support | ✅ Full | ❌ Requires network |
| Multi-user | ❌ Single user | ✅ Full |
| Cost | Free (static hosting) | Pay per invocation |
| Best For | Demos, prototypes, docs | Production applications |
- Plugin System — MSW and Hono server plugins
- Client SDK — Frontend data fetching
- Driver Configuration — Database setup
- Architecture — Kernel and runtime overview