Skip to content

Latest commit

 

History

History
389 lines (304 loc) · 13.9 KB

File metadata and controls

389 lines (304 loc) · 13.9 KB
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 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).

**Updated for 11.** The in-browser **MSW Mode** (`@objectstack/plugin-msw`) and the **Next.js adapter** (`@objectstack/nextjs`) were **removed in 11** and are no longer published. Use the Hono server path below. The "MSW Mode" how-to further down is retained for reference only and is slated for removal.
Mode Runtime Vercel Feature Use Case
Server (default) Node.js / Edge Serverless Functions Production apps, Studio, real database

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

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

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

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

# 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'],
  },
});

vercel.json

{
  "$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" }
  ]
}
The `rewrites` rule is essential for SPA routing — it ensures all paths serve `index.html` so the client-side router can handle navigation.

Monorepo Configuration

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


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        │
│  └───────────────────────────────────┘                        │
│                                                               │
└───────────────────────────────────────────────────────────────┘

Hono serverless function (@objectstack/hono)

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.

The published ObjectStack Studio/console (`@object-ui/console`) does **not** use this topology. It deploys as a pure static SPA with no `api/` function — it is meant to be embedded in, or pointed at, a separate ObjectStack server via `VITE_SERVER_URL`. For that separate server, prefer the CLI (`objectstack serve`) over a hand-rolled Hono function.

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" }
  ]
}
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.

Environment Variables

Configure these in Vercel Project Settings → Environment Variables:

Studio / SPA build

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)

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

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.

**Never commit secrets.** Use Vercel's environment variable UI or the Vercel CLI (`vercel env add`) to configure tokens and S3 credentials.

Choosing the Mode

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 at VITE_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.


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
  • [ ] vercel.json sets VITE_USE_MOCK_SERVER=false 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_USE_MOCK_SERVER=true 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