---
title: Deploy to Vercel
description: Deploy ObjectStack applications to Vercel — Server mode (recommended) for real API endpoints or MSW mode for static demos
---

# Deploy to Vercel

ObjectStack supports two deployment modes on Vercel. **Server mode is recommended** for production and is the default for ObjectStack Studio.

| Mode | Runtime | Vercel Feature | Use Case |
| :--- | :--- | :--- | :--- |
| **Server** (default) | Node.js / Edge | Serverless Functions | Production apps, Studio, real database |
| **MSW** | Browser (Service Worker) | Static Site | Offline-only demos, prototypes |

---

## MSW Mode (Static SPA)

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.

### How It Works

```
┌─────────────────────────── Browser ───────────────────────────┐
│  React App  →  fetch('/api/v1/data/task')                     │
│       ↓                                                       │
│  MSW Service Worker  (intercepts request)                     │
│       ↓                                                       │
│  ObjectKernel  →  ObjectQL  →  InMemoryDriver                 │
│       ↓                                                       │
│  JSON Response  →  React App                                  │
└───────────────────────────────────────────────────────────────┘
```

### Project Setup

```typescript
// 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' },
      ],
    },
  ],
});
```

### Kernel Bootstrap (Browser)

Create a kernel factory that boots the entire stack in the browser:

```typescript
// 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;
}
```

### Entry Point

```typescript
// 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();
```

### Vite Configuration

MSW requires the Service Worker file in your `public/` directory. Add the init script and configure Vite:

```bash
# Generate the MSW Service Worker file
npx msw init public --save
```

```typescript
// 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'],
  },
});
```

### `vercel.json`

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "framework": "vite",
  "buildCommand": "vite build",
  "outputDirectory": "dist",
  "build": {
    "env": {
      "VITE_RUNTIME_MODE": "msw"
    }
  },
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}
```

<Callout type="info">
The `rewrites` rule is essential for SPA routing — it ensures all paths serve `index.html` so the client-side router can handle navigation.
</Callout>

### Monorepo Configuration

If your project lives in a monorepo (e.g. pnpm workspaces + Turborepo), update the install and build commands:

```json
{
  "$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_RUNTIME_MODE": "msw"
    }
  },
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}
```

Set the **Root Directory** in Vercel project settings to the app's folder (e.g. `apps/my-app`).

---

## Server Mode (Serverless Functions)

In Server mode, ObjectStack runs inside Vercel Serverless Functions. API requests are handled by real backend logic with a real database.

### Architecture

```
┌─── Vercel ────────────────────────────────────────────────────┐
│                                                               │
│  Static Assets (React SPA)                                    │
│  ┌───────────────────────────────────┐                        │
│  │  /index.html, /assets/*           │                        │
│  └───────────────────────────────────┘                        │
│                                                               │
│  Serverless Functions                                         │
│  ┌───────────────────────────────────┐                        │
│  │  /api/[...objectstack]            │                        │
│  │    → ObjectKernel                 │                        │
│  │    → ObjectQL                     │                        │
│  │    → PostgreSQL / MongoDB / ...   │──── External DB        │
│  └───────────────────────────────────┘                        │
│                                                               │
└───────────────────────────────────────────────────────────────┘
```

### Option A: Next.js + `@objectstack/nextjs`

This is the recommended approach for Vercel. The `@objectstack/nextjs` adapter maps all ObjectStack protocol endpoints to a single Next.js catch-all route.

**1. Create the kernel singleton:**

```typescript
// lib/kernel.ts
import { ObjectKernel, DriverPlugin, AppPlugin } from '@objectstack/runtime';
import { ObjectQLPlugin } from '@objectstack/objectql';
import appConfig from '../objectstack.config';

let kernel: ObjectKernel | null = null;

export async function getKernel() {
  if (kernel) return kernel;

  kernel = new ObjectKernel();
  await kernel.use(new ObjectQLPlugin());

  // Use your production driver (Postgres, MongoDB, etc.)
  // await kernel.use(new DriverPlugin(new PostgresDriver({
  //   url: process.env.DATABASE_URL,
  // })));

  // Load the application configuration (objects, data, etc.)
  await kernel.use(new AppPlugin(appConfig));

  await kernel.bootstrap();
  return kernel;
}
```

**2. Create the API route handler:**

```typescript
// app/api/[...objectstack]/route.ts
import { createRouteHandler } from '@objectstack/nextjs';
import { getKernel } from '@/lib/kernel';

async function handler(...args: any[]) {
  const kernel = await getKernel();
  const routeHandler = createRouteHandler({ kernel, prefix: '/api' });
  return routeHandler(...args);
}

export { handler as GET, handler as POST, handler as PATCH, handler as DELETE };
```

**3. `vercel.json` (optional — Next.js works out of the box):**

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "framework": "nextjs"
}
```

### Option B: Hono + `@objectstack/hono` (Vite SPA)

This is what ObjectStack Studio uses. The Vite 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):

```typescript
// 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`):

```typescript
// 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`:**

```json
{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "framework": "vite",
  "buildCommand": "vite build",
  "outputDirectory": "dist",
  "build": {
    "env": {
      "VITE_RUNTIME_MODE": "server",
      "VITE_SERVER_URL": ""
    }
  },
  "rewrites": [
    { "source": "/api/(.*)", "destination": "/api" },
    { "source": "/((?!api/).*)", "destination": "/index.html" }
  ]
}
```

<Callout type="info">
Setting `VITE_SERVER_URL` to empty string tells the client SDK to use same-origin API calls. The first rewrite routes all `/api/*` sub-paths to the `api/index.ts` serverless function. The second rewrite excludes `/api/` paths so all other paths serve the SPA.
</Callout>

---

## Environment Variables

Configure these in Vercel Project Settings → Environment Variables:

### Studio / SPA build

| Variable | Description |
| :--- | :--- |
| `VITE_RUNTIME_MODE` | `msw` (in-browser) or `server` (real backend) |
| `VITE_SERVER_URL` | Backend API URL (empty for same-origin) |

### Cloud control plane

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 |
| :--- | :--- | :--- |
| `AUTH_SECRET` | yes | ≥ 32 chars |
| `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 |

> See [Publish, Versioning & Preview](./publish-and-preview) for the full
> storage matrix (native S3, Cloudflare R2, MinIO, etc.) and what the cloud
> control plane stores in each bucket.

<Callout type="warn">
**Never commit secrets.** Use Vercel's environment variable UI or the Vercel CLI (`vercel env add`) to configure tokens and S3 credentials.
</Callout>

---

## Runtime Mode Switching

ObjectStack Studio supports switching between MSW and Server mode at runtime using a URL parameter:

```
https://myapp.vercel.app?mode=msw     → In-browser kernel
https://myapp.vercel.app?mode=server  → Real backend
```

This is controlled by the config module which checks (in priority order):
1. `?mode=` URL parameter
2. `VITE_RUNTIME_MODE` environment variable
3. Embedded detection (`/_studio/` path)
4. Default: `msw`

---

## Deployment Checklist

### Server Mode (Recommended)

- [ ] `api/index.ts` Hono entrypoint exists with `handle(app)` export
- [ ] `api/_kernel.ts` boots the kernel with the correct driver and broker shim
- [ ] `vercel.json` sets `VITE_RUNTIME_MODE=server` and `VITE_SERVER_URL=` (empty)
- [ ] Rewrite rule routes `/api/*` to `/api` and excludes `/api/` from SPA rewrite
- [ ] `DATABASE_URL` is configured in Vercel environment variables (for production drivers)
- [ ] CORS is configured if frontend and API are on different origins

### MSW Mode (Static Demo)

- [ ] `msw init public --save` has been run (Service Worker in `public/`)
- [ ] `vercel.json` specifies `"framework": "vite"` and SPA rewrites
- [ ] `VITE_RUNTIME_MODE=msw` is set in build environment
- [ ] Seed data is defined in `objectstack.config.ts` (`data` array)

---

## Comparison

| 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 |

---

## Related

- [Plugin System](/docs/guides/plugins) — MSW and Hono server plugins
- [Client SDK](/docs/guides/client-sdk) — Frontend data fetching
- [Driver Configuration](/docs/guides/driver-configuration) — Database setup
- [Architecture](/docs/getting-started/architecture) — Kernel and runtime overview
