Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`apps/demo`** — added explicit `@objectstack/spec` and `zod` devDependencies as defense-in-depth for Vercel deployment.
- **`@objectql/types`** — moved `@objectstack/spec` and `zod` from `devDependencies` to `dependencies`. The compiled JS output contains runtime imports of `@objectstack/spec` (via `z.infer<typeof Data.X>` patterns), so they must be declared as production dependencies.

### Changed

- **`apps/demo`** — switched default data driver from `@objectstack/driver-memory` (InMemoryDriver) to `@objectql/driver-turso` (TursoDriver). When `TURSO_DATABASE_URL` is set, the demo uses a persistent Turso/libSQL database; otherwise falls back to InMemoryDriver for zero-config local development.
- `objectstack.config.ts` — environment-aware `createDefaultDriver()` selects Turso or MemoryDriver.
- `api/[[...route]].ts` — Vercel serverless handler uses TursoDriver with `TURSO_DATABASE_URL`, `TURSO_AUTH_TOKEN`, `TURSO_SYNC_URL`, and `TURSO_SYNC_INTERVAL` env vars.
- `scripts/build-vercel.sh` — now builds `@objectql/driver-turso` alongside other drivers.
- `README.md` — documents new Turso environment variables and architecture.

### Added

- **`apps/demo`** — standalone Vercel-deployable demo application ([#issue](https://github.com/objectstack-ai/objectql/issues)):
Expand Down
11 changes: 8 additions & 3 deletions apps/demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Runs locally with `@objectstack/cli` and deploys to **Vercel** as a serverless f

## Features

- **In-memory driver** — zero external database required; data persists across warm Vercel invocations.
- **Turso/libSQL driver** — persistent, edge-first SQLite via `@objectql/driver-turso` when `TURSO_DATABASE_URL` is set.
- **In-memory fallback** — zero external database required for quick local development.
- **Console UI** — full ObjectStack Console available at `/console/`.
- **Studio UI** — ObjectStack Studio available at `/_studio/`.
- **Project-Tracker showcase** — ships with the `examples/showcase/project-tracker` metadata (objects, views, permissions) so the demo has real data structures out of the box.
Expand Down Expand Up @@ -44,6 +45,10 @@ The development server starts on `http://localhost:3000`.
|---|---|---|
| `AUTH_SECRET` | **Yes** (production) | Secret key for signing auth tokens. Generate with `openssl rand -base64 32`. |
| `AUTH_TRUSTED_ORIGINS` | No | Comma-separated list of additional trusted origins (e.g. `https://myapp.example.com`). |
| `TURSO_DATABASE_URL` | No | Turso/libSQL database URL (e.g. `libsql://my-db-org.turso.io`). When set, uses Turso instead of in-memory driver. |
| `TURSO_AUTH_TOKEN` | When using Turso | JWT auth token for the Turso database. |
| `TURSO_SYNC_URL` | No | Remote sync URL for embedded replica mode. |
| `TURSO_SYNC_INTERVAL` | No | Periodic sync interval in seconds (default: 60). |

4. Deploy:

Expand All @@ -58,7 +63,7 @@ vercel --cwd apps/demo --prod
### How It Works

- **`vercel.json`** — Configures Vercel to use a custom build command, allocate 1 GiB memory to the serverless function, and rewrite all requests to the catch-all `api/[[...route]].ts` handler.
- **`api/[[...route]].ts`** — Bootstraps the full ObjectStack kernel with ObjectQL plugins, the in-memory driver, auth, Console, and Studio. Uses `@hono/node-server`'s `getRequestListener()` to bridge the Vercel serverless runtime with the Hono HTTP framework.
- **`api/[[...route]].ts`** — Bootstraps the full ObjectStack kernel with ObjectQL plugins, the Turso driver (or in-memory fallback), auth, Console, and Studio. Uses `@hono/node-server`'s `getRequestListener()` to bridge the Vercel serverless runtime with the Hono HTTP framework.
- **`scripts/build-vercel.sh`** — Builds all required workspace packages (foundation, drivers, plugins, protocols, examples) in the correct dependency order.
- **`scripts/patch-symlinks.cjs`** — Replaces pnpm workspace symlinks with real copies so Vercel can bundle the function without symlink errors.

Expand Down Expand Up @@ -110,5 +115,5 @@ apps/demo/
ObjectStack Kernel
(ObjectQL + Auth +
InMemoryDriver)
TursoDriver)
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The architecture diagram now shows TursoDriver unconditionally, but this demo still falls back to the in-memory driver when TURSO_DATABASE_URL is not set. Update the diagram to reflect the conditional driver choice (e.g., “TursoDriver / InMemoryDriver”) to keep the docs consistent with the behavior described above.

Suggested change
TursoDriver)
TursoDriver / InMemoryDriver)

Copilot uses AI. Check for mistakes.
```
51 changes: 42 additions & 9 deletions apps/demo/api/[[...route]].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
* Vercel Serverless Function — ObjectQL Demo Handler
*
* Bootstraps the ObjectStack kernel with ObjectQL plugins and the
* project-tracker demo metadata, using @objectstack/driver-memory
* for zero-config in-memory data.
* project-tracker demo metadata, using @objectql/driver-turso when
* TURSO_DATABASE_URL is set, or @objectstack/driver-memory as a
* zero-config fallback.
*
* Uses `getRequestListener()` from `@hono/node-server` together with
* an `extractBody()` helper to handle Vercel's pre-buffered request
Expand All @@ -14,8 +15,10 @@
* a fresh `Request` object prevents POST/PUT/PATCH requests (e.g.
* login) from hanging indefinitely.
*
* Data lives in the function instance's memory and persists across
* warm invocations (Vercel Fluid Compute) but resets on cold start.
* When using Turso, data is persisted in the Turso cloud database.
* When using InMemoryDriver, data lives in the function instance's
* memory and persists across warm invocations (Vercel Fluid Compute)
* but resets on cold start.
*
* Both Console (/) and Studio (/_studio/) UIs are served as static SPAs.
*
Expand All @@ -30,6 +33,7 @@ import { ObjectKernel, DriverPlugin, AppPlugin, createDispatcherPlugin, createRe
import { HonoHttpServer } from '@objectstack/plugin-hono-server';
import { AuthPlugin } from '@objectstack/plugin-auth';
import { InMemoryDriver } from '@objectstack/driver-memory';
import { createTursoDriver, type TursoDriver } from '@objectql/driver-turso';
import { ObjectQLPlugin } from '@objectstack/objectql';
import { getRequestListener } from '@hono/node-server';
import type { Hono } from 'hono';
Expand Down Expand Up @@ -278,10 +282,32 @@ async function bootstrap(): Promise<Hono> {
await withTimeout(kernel.use(new ObjectQLPlugin()), PLUGIN_TIMEOUT_MS, 'ObjectQLPlugin');
log('ObjectQLPlugin registered.');

// 2. In-memory data driver (no external DB required)
log('Registering DriverPlugin (InMemoryDriver)…');
await withTimeout(kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')), PLUGIN_TIMEOUT_MS, 'DriverPlugin');
log('DriverPlugin registered.');
// 2. Data driver — Turso when TURSO_DATABASE_URL is set, InMemoryDriver otherwise
const tursoUrl = process.env.TURSO_DATABASE_URL;
let tursoDriver: TursoDriver | null = null;
if (tursoUrl) {
log(`Registering TursoDriver (${tursoUrl})…`);
const syncUrl = process.env.TURSO_SYNC_URL;
tursoDriver = createTursoDriver({
url: tursoUrl,
authToken: process.env.TURSO_AUTH_TOKEN,
syncUrl,
sync: syncUrl
? {
intervalSeconds: Number(process.env.TURSO_SYNC_INTERVAL) || 60,
onConnect: true,
}
: undefined,
Comment on lines +301 to +306
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TURSO_SYNC_INTERVAL parsing uses Number(...) || 60, which forces 0 to become 60. Turso supports intervalSeconds = 0 (manual-only), so this makes it impossible to disable periodic sync via env var. Use an explicit finite-number check and only default when unset/invalid, preserving 0.

Copilot uses AI. Check for mistakes.
});
// DriverPlugin from @objectstack/runtime expects the upstream Driver interface;
// TursoDriver implements @objectql/types Driver which is structurally compatible.
await withTimeout(kernel.use(new DriverPlugin(tursoDriver as any, 'turso')), PLUGIN_TIMEOUT_MS, 'DriverPlugin-turso');
log('TursoDriver registered.');
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing tursoDriver as any into DriverPlugin removes type safety and can mask a real interface mismatch between @objectql/types Driver and what @objectstack/runtime expects. Prefer adapting the Turso driver to the expected driver type (e.g., via a small wrapper/adapter with the required surface area) or importing and using the exact Driver type from @objectstack/runtime to ensure compatibility at compile time.

Copilot uses AI. Check for mistakes.
} else {
log('Registering DriverPlugin (InMemoryDriver)…');
await withTimeout(kernel.use(new DriverPlugin(new InMemoryDriver(), 'memory')), PLUGIN_TIMEOUT_MS, 'DriverPlugin-memory');
log('InMemoryDriver registered.');
}

// 3. HTTP server adapter — register the Hono app without TCP listener
const httpServer = new HonoHttpServer();
Expand Down Expand Up @@ -445,7 +471,14 @@ async function bootstrap(): Promise<Hono> {
log('Studio SPA registered.');
}

// 12. Bootstrap kernel (init + start all plugins, fire kernel:ready)
// 12. Connect Turso driver (if applicable) before kernel bootstrap
if (tursoDriver) {
log('Connecting TursoDriver…');
await withTimeout(tursoDriver.connect(), PLUGIN_TIMEOUT_MS, 'TursoDriver.connect()');
log('TursoDriver connected.');
}

// 13. Bootstrap kernel (init + start all plugins, fire kernel:ready)
log('Running kernel.bootstrap()…');
await withTimeout(kernel.bootstrap(), KERNEL_BOOTSTRAP_TIMEOUT_MS, 'kernel.bootstrap()');
log(`Bootstrap complete in ${elapsed()}.`);
Expand Down
42 changes: 36 additions & 6 deletions apps/demo/objectstack.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/**
* ObjectQL Demo — Application Configuration
*
* Minimal ObjectStack configuration for the demo application.
* Uses in-memory driver with the project-tracker showcase example.
* ObjectStack configuration for the demo application.
* Uses @objectql/driver-turso when TURSO_DATABASE_URL is set,
* falls back to MemoryDriver for zero-config local development.
*
* For local development: `pnpm dev` (uses @objectstack/cli)
* For Vercel deployment: configured via api/[[...route]].ts
Expand All @@ -29,10 +30,33 @@ import { FormulaPlugin } from '@objectql/plugin-formula';
import { ObjectQLSecurityPlugin } from '@objectql/plugin-security';
import { createApiRegistryPlugin } from '@objectstack/core';
import { MemoryDriver } from '@objectql/driver-memory';
import { createTursoDriver } from '@objectql/driver-turso';
import { createAppPlugin } from '@objectql/platform-node';

// In-memory driver — zero-config, no external DB required.
const defaultDriver = new MemoryDriver();
// Choose driver based on environment — Turso when TURSO_DATABASE_URL is set,
// MemoryDriver otherwise (zero-config fallback for quick starts).
function createDefaultDriver() {
const tursoUrl = process.env.TURSO_DATABASE_URL;
if (tursoUrl) {
console.log(`🗄️ Driver: Turso (${tursoUrl})`);
const syncUrl = process.env.TURSO_SYNC_URL;
return createTursoDriver({
url: tursoUrl,
authToken: process.env.TURSO_AUTH_TOKEN,
syncUrl,
sync: syncUrl
? {
intervalSeconds: Number(process.env.TURSO_SYNC_INTERVAL) || 60,
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TURSO_SYNC_INTERVAL parsing uses Number(...) || 60, which forces 0 to become 60. The Turso driver config explicitly allows intervalSeconds = 0 (manual-only), so this prevents disabling periodic sync. Parse with an explicit finite-number check and default only when the env var is missing/invalid, while preserving 0.

Suggested change
return createTursoDriver({
url: tursoUrl,
authToken: process.env.TURSO_AUTH_TOKEN,
syncUrl,
sync: syncUrl
? {
intervalSeconds: Number(process.env.TURSO_SYNC_INTERVAL) || 60,
const rawSyncInterval = process.env.TURSO_SYNC_INTERVAL;
const parsedSyncInterval =
rawSyncInterval !== undefined ? Number(rawSyncInterval) : NaN;
const syncIntervalSeconds = Number.isFinite(parsedSyncInterval)
? parsedSyncInterval
: 60;
return createTursoDriver({
url: tursoUrl,
authToken: process.env.TURSO_AUTH_TOKEN,
syncUrl,
sync: syncUrl
? {
intervalSeconds: syncIntervalSeconds,

Copilot uses AI. Check for mistakes.
onConnect: true,
}
: undefined,
});
}
console.log('🗄️ Driver: Memory (in-memory, non-persistent)');
return new MemoryDriver();
}

const defaultDriver = createDefaultDriver();

// Load the project-tracker showcase metadata.
const projectTrackerPlugin = createAppPlugin({
Expand All @@ -51,13 +75,19 @@ export default {
createApiRegistryPlugin(),
new HonoServerPlugin({}),
new ConsolePlugin(),
// Register the driver as 'driver.default' service.
// Register the active driver as 'driver.default' service so upstream
// ObjectQLPlugin can discover it during start() phase.
{
name: 'driver-default',
init: async (ctx: any) => {
ctx.registerService('driver.default', defaultDriver);
},
start: async () => {},
start: async () => {
// Connect Turso driver if applicable (MemoryDriver.connect() is a no-op)
if (typeof (defaultDriver as any).connect === 'function') {
await (defaultDriver as { connect: () => Promise<void> }).connect();
}
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The start hook uses (defaultDriver as any) and a runtime typeof ...connect check, but both MemoryDriver and TursoDriver expose async connect(). Prefer typing defaultDriver against a shared interface/type and calling await defaultDriver.connect() directly to avoid any and keep this type-safe.

Copilot uses AI. Check for mistakes.
},
},
projectTrackerPlugin,
new ObjectQLPlugin(),
Expand Down
1 change: 1 addition & 0 deletions apps/demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@object-ui/console": "^3.1.3",
"@objectql/core": "workspace:*",
"@objectql/driver-memory": "workspace:*",
"@objectql/driver-turso": "workspace:*",
"@objectql/example-project-tracker": "workspace:*",
"@objectql/platform-node": "workspace:*",
"@objectql/plugin-formula": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions apps/demo/scripts/build-vercel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pnpm --filter @objectql/platform-node build

echo "▸ Building drivers…"
pnpm --filter @objectql/driver-memory \
--filter @objectql/driver-turso \
--filter @objectql/driver-sql \
build

Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading