Skip to content

Commit 90aea6a

Browse files
committed
feat: Implement control-plane preset and node entry point
- Added control-plane preset in `control-plane-preset.ts` to manage plugins for the shared server's control plane. - Introduced a Node.js entry point in `node-entry.ts` for local development and deployments, utilizing the existing Hono app. - Created documentation for cloud deployment and multi-project configurations in `cloud-deployment.mdx`. - Established ADR-0004 to detail the architecture for cloud control plane and per-project kernels. - Developed a kernel manager in `kernel-manager.ts` to handle per-project kernel instances with LRU and TTL caching. - Implemented a project kernel factory in `project-kernel-factory.ts` to create project-specific kernels with appropriate drivers and plugins. - Added tests for the kernel manager to ensure proper caching, eviction, and error handling.
1 parent cc32130 commit 90aea6a

26 files changed

Lines changed: 1962 additions & 133 deletions

apps/server/.dockerignore

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Paths here are evaluated relative to the build context (repo root).
2+
# Keep image slim by excluding every workspace member that apps/server
3+
# does not depend on + anything that gets regenerated during build.
4+
5+
# ─── Node / pnpm caches ──────────────────────────────────────────────
6+
**/node_modules
7+
**/.pnpm-store
8+
**/.pnpm
9+
10+
# ─── Build output that Docker should not carry from the host ────────
11+
**/dist
12+
**/.turbo
13+
**/.next
14+
**/.vercel
15+
**/.cache
16+
**/build
17+
18+
# ─── Version control / editor ────────────────────────────────────────
19+
.git
20+
.gitignore
21+
.github
22+
.vscode
23+
.idea
24+
25+
# ─── Local env / secrets ─────────────────────────────────────────────
26+
**/.env
27+
**/.env.*
28+
!**/.env.example
29+
30+
# ─── Tests / scratch data ────────────────────────────────────────────
31+
**/coverage
32+
**/*.log
33+
**/.DS_Store
34+
**/tmp
35+
**/.objectstack
36+
**/data
37+
38+
# ─── Workspace members not needed by apps/server at runtime ─────────
39+
apps/studio
40+
apps/docs
41+
content/docs
42+
skills
43+
docs
44+
45+
# ─── Misc ────────────────────────────────────────────────────────────
46+
README.md
47+
CHANGELOG.md
48+
LICENSE

apps/server/Dockerfile

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# syntax=docker/dockerfile:1.7
2+
#
3+
# ObjectStack Server — production container image
4+
# --------------------------------------------------
5+
# Build with the **repository root** as the build context so the entire pnpm
6+
# workspace (packages, examples, apps/server itself) is available to the
7+
# builder stage. Example:
8+
#
9+
# docker build -f apps/server/Dockerfile -t objectstack/server:dev .
10+
# fly deploy --dockerfile apps/server/Dockerfile
11+
#
12+
# At runtime the container reads project config from env vars (see
13+
# apps/server/README.md for the full list) and optionally mounts a
14+
# persistent volume at /data for the local-SQLite control plane.
15+
16+
# ──────────────────────────────────────────────────────────────────────────
17+
# Stage 1 — install workspace deps + build required packages
18+
# ──────────────────────────────────────────────────────────────────────────
19+
FROM node:22-slim AS builder
20+
21+
# native bindings for better-sqlite3 and friends
22+
RUN apt-get update \
23+
&& apt-get install -y --no-install-recommends \
24+
python3 make g++ ca-certificates \
25+
&& rm -rf /var/lib/apt/lists/*
26+
27+
WORKDIR /repo
28+
RUN corepack enable
29+
30+
# Bring in lockfile + workspace manifest first to maximise layer cache hits
31+
# on subsequent rebuilds where only source code changes.
32+
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json tsup.config.ts ./
33+
34+
# Copy every workspace member required at runtime.
35+
# Studio / docs are excluded to keep the image slim (see .dockerignore).
36+
COPY packages ./packages
37+
COPY examples ./examples
38+
COPY apps/server ./apps/server
39+
40+
RUN pnpm install --frozen-lockfile --prod=false
41+
42+
# Build upstream packages that apps/server depends on via workspace: protocol.
43+
# `...` expands to "package + all transitive deps in the workspace".
44+
RUN pnpm --filter '@objectstack/server...' build || true
45+
46+
47+
# ──────────────────────────────────────────────────────────────────────────
48+
# Stage 2 — slim runtime image
49+
# ──────────────────────────────────────────────────────────────────────────
50+
FROM node:22-slim AS runner
51+
52+
RUN apt-get update \
53+
&& apt-get install -y --no-install-recommends ca-certificates wget \
54+
&& rm -rf /var/lib/apt/lists/* \
55+
&& corepack enable
56+
57+
WORKDIR /app
58+
59+
ENV NODE_ENV=production \
60+
PORT=3000 \
61+
HOST=0.0.0.0
62+
63+
# Copy the full workspace (with built dist/ directories) and the shared
64+
# node_modules tree. pnpm uses hard-link dedup so the transfer is cheap.
65+
COPY --from=builder /repo /app
66+
67+
EXPOSE 3000
68+
VOLUME ["/data"]
69+
70+
# The container talks to the control plane (local SQLite at /data or remote
71+
# Turso) via env. Override any of these in fly.toml / `docker run -e …`.
72+
ENV OBJECTSTACK_CONTROL_DB=/data/control.db
73+
74+
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
75+
CMD wget -qO- "http://127.0.0.1:${PORT}/api/v1/health" >/dev/null 2>&1 || exit 1
76+
77+
WORKDIR /app/apps/server
78+
CMD ["pnpm", "start"]

apps/server/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,69 @@ See [DEPLOYMENT.md](./DEPLOYMENT.md) for detailed deployment instructions.
1717
- **Zero-Code Backend**: No creating routes or controllers per object.
1818
- **Preview Mode**: Run in demo mode — bypass login, auto-simulate admin identity.
1919
- **Vercel Deployment**: Ready-to-deploy to Vercel with Hono adapter.
20+
- **Dual-mode bootstrap**: single self-hosted kernel *or* per-project kernels fronting a cloud control plane (see below).
21+
22+
## Bootstrap shapes
23+
24+
A single `apps/server` process can run in three shapes, selected by env vars.
25+
See the [Cloud vs Self-Hosted guide](../../content/docs/guides/cloud-deployment.mdx) for the full walkthrough.
26+
27+
### `single` (default)
28+
29+
One `ObjectKernel`, one database, every plugin from `objectstack.config.ts`.
30+
No control plane, no per-project routing. Ideal for local development and
31+
single-project deployments.
32+
33+
```bash
34+
OBJECTSTACK_DATABASE_URL=file:./local.db \
35+
AUTH_SECRET=$(openssl rand -hex 32) \
36+
pnpm dev
37+
# → http://localhost:3000
38+
```
39+
40+
### `multi-project-local`
41+
42+
Control-plane sys_* tables live in a local SQLite file. Projects are created
43+
via Studio / the `/api/v1/cloud/projects` REST endpoint; each project binds
44+
its own driver (sqlite / Turso / postgres). Hostname-based routing delivers
45+
each request to the matching project's kernel.
46+
47+
```bash
48+
OBJECTSTACK_MULTI_PROJECT=true \
49+
OBJECTSTACK_CONTROL_DB=/data/control.db \
50+
AUTH_SECRET=$(openssl rand -hex 32) \
51+
pnpm dev
52+
# → http://localhost:3000, routes by Host header → sys_project.hostname
53+
```
54+
55+
### `multi-project-remote`
56+
57+
Same path as `multi-project-local` but the control-plane driver points at a
58+
remote Turso database. Used for SaaS deployments where many replicas share
59+
one sys_* schema.
60+
61+
```bash
62+
OBJECTSTACK_CONTROL_PLANE_URL=https://control.example.com \
63+
TURSO_DATABASE_URL=libsql://control.turso.io \
64+
TURSO_AUTH_TOKEN=... \
65+
AUTH_SECRET=$(openssl rand -hex 32) \
66+
pnpm dev
67+
```
68+
69+
| Env var | Purpose |
70+
| --------------------------------- | ------------------------------------------------------------------------ |
71+
| `OBJECTSTACK_MULTI_PROJECT` | Set to `true` to enable local multi-project mode. |
72+
| `OBJECTSTACK_CONTROL_DB` | Path to the local SQLite control DB (default `./.objectstack/data/control.db`). |
73+
| `OBJECTSTACK_CONTROL_PLANE_URL` | Set to enable remote multi-project mode (value identifies the control plane). |
74+
| `TURSO_DATABASE_URL` | Turso URL for the control DB (required with `OBJECTSTACK_CONTROL_PLANE_URL`). |
75+
| `TURSO_AUTH_TOKEN` | Auth token for the control DB. |
76+
| `OBJECTSTACK_KERNEL_CACHE_SIZE` | LRU size for per-project kernels (default 32). |
77+
| `OBJECTSTACK_KERNEL_TTL_MS` | Idle eviction TTL in ms (default 900000). |
78+
| `AUTH_SECRET` | Better Auth session secret (≥ 32 chars). |
79+
80+
In both multi-project shapes, `OBJECTSTACK_DATABASE_URL` is **not** read on
81+
the data plane — project database URLs and credentials come from sys_project
82+
rows.
2083

2184
## Setup
2285

apps/server/fly.toml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# fly.toml — ObjectStack Server (multi-project shape)
2+
#
3+
# Deploy from the repo root so the monorepo Dockerfile can see the full
4+
# workspace:
5+
#
6+
# fly deploy --config apps/server/fly.toml \
7+
# --dockerfile apps/server/Dockerfile
8+
#
9+
# Wildcard subdomain TLS (project-per-subdomain routing):
10+
#
11+
# fly certs add "*.objectstack.app" --app objectstack-server
12+
# # follow the _acme-challenge DNS instructions in the output
13+
#
14+
# Secrets (never commit — use `fly secrets set …`):
15+
# AUTH_SECRET, OBJECTSTACK_COOKIE_DOMAIN,
16+
# TURSO_ORG_NAME, TURSO_API_TOKEN,
17+
# (optional) TURSO_DATABASE_URL + TURSO_AUTH_TOKEN for multi-project-remote
18+
19+
app = "objectstack-server"
20+
primary_region = "iad"
21+
kill_signal = "SIGTERM"
22+
kill_timeout = "30s"
23+
24+
[build]
25+
dockerfile = "Dockerfile"
26+
27+
[http_service]
28+
internal_port = 3000
29+
force_https = true
30+
auto_stop_machines = true
31+
auto_start_machines = true
32+
min_machines_running = 1
33+
processes = ["app"]
34+
35+
[http_service.concurrency]
36+
type = "requests"
37+
hard_limit = 250
38+
soft_limit = 200
39+
40+
[[http_service.checks]]
41+
method = "GET"
42+
path = "/api/v1/health"
43+
interval = "30s"
44+
timeout = "5s"
45+
grace_period = "30s"
46+
47+
[[vm]]
48+
size = "shared-cpu-2x"
49+
memory = "1gb"
50+
51+
# Persistent volume for the local-SQLite control DB (multi-project-local).
52+
# Attach with `fly volumes create objectstack_data --size 3 --region iad`.
53+
[mounts]
54+
source = "objectstack_data"
55+
destination = "/data"
56+
57+
[env]
58+
# Enable multi-project-local mode by default — control plane lives in the
59+
# SQLite file on the attached volume. Override to multi-project-remote by
60+
# setting OBJECTSTACK_CONTROL_PLANE_URL + TURSO_DATABASE_URL via secrets.
61+
OBJECTSTACK_MULTI_PROJECT = "true"
62+
OBJECTSTACK_CONTROL_DB = "/data/control.db"
63+
OBJECTSTACK_KERNEL_CACHE_SIZE = "50"
64+
OBJECTSTACK_KERNEL_TTL_MS = "1800000"
65+
OBJECTSTACK_ENV_CACHE_TTL_MS = "300000"
66+
PORT = "3000"
67+
HOST = "0.0.0.0"

apps/server/objectstack.config.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* Shared ObjectStack Server Configuration
4+
* Single-Kernel Server Configuration
55
*
6-
* Single source of truth for all plugins — used by both:
7-
* - `objectstack serve` (local dev via CLI)
8-
* - `server/index.ts` (Vercel serverless deployment)
6+
* Used only by the legacy **single** bootstrap shape (no env flags set).
7+
* In multi-project modes — whether local (`OBJECTSTACK_MULTI_PROJECT=true`)
8+
* or remote (`OBJECTSTACK_CONTROL_PLANE_URL=...`) — the plugin list here is
9+
* ignored: the control plane uses `createControlPlanePlugins()` and each
10+
* project kernel is assembled by `DefaultProjectKernelFactory` from the
11+
* base-plugin factory wired in `bootstrap.ts`.
12+
*
13+
* See `server/bootstrap.ts` for shape selection logic.
914
*/
1015

1116
import { defineStack } from '@objectstack/spec';
12-
import { AppPlugin, DriverPlugin, createSystemProjectPlugin } from '@objectstack/runtime';
17+
import { AppPlugin, DriverPlugin } from '@objectstack/runtime';
1318
import { ObjectQLPlugin } from '@objectstack/objectql';
1419
import { InMemoryDriver } from '@objectstack/driver-memory';
1520
import { TursoDriver } from '@objectstack/driver-turso';
@@ -22,8 +27,6 @@ import { MetadataPlugin } from '@objectstack/metadata';
2227
import { AIServicePlugin } from '@objectstack/service-ai';
2328
import { AutomationServicePlugin } from '@objectstack/service-automation';
2429
import { AnalyticsServicePlugin } from '@objectstack/service-analytics';
25-
import { PackageServicePlugin } from '@objectstack/service-package';
26-
import { createTenantPlugin } from '@objectstack/service-tenant';
2730
import CrmApp from '../../examples/app-crm/objectstack.config';
2831
import TodoApp from '../../examples/app-todo/objectstack.config';
2932
import BiPluginManifest from '../../examples/plugin-bi/objectstack.config';
@@ -46,11 +49,18 @@ const tursoDriver = new TursoDriver(
4649
: { url: `file:${resolve(__dirname, '.objectstack/data/dev.db')}` },
4750
);
4851

49-
// Datasource routing: sys namespace → turso, everything else → memory
50-
const datasourceMapping = [
51-
{ namespace: 'sys', datasource: 'com.objectstack.driver.turso' },
52-
{ default: true, datasource: 'com.objectstack.driver.memory' },
53-
];
52+
// Datasource routing: default → memory for self-hosted data plane.
53+
// sys_* namespaces are handled by the control-plane preset in multi-project
54+
// shapes; in single-kernel mode the tenant/auth tables are bootstrapped via
55+
// turso when TURSO_DATABASE_URL is configured.
56+
const datasourceMapping = process.env.TURSO_DATABASE_URL
57+
? [
58+
{ namespace: 'sys', datasource: 'com.objectstack.driver.turso' },
59+
{ default: true, datasource: 'com.objectstack.driver.memory' },
60+
]
61+
: [
62+
{ default: true, datasource: 'com.objectstack.driver.memory' },
63+
];
5464

5565
const oqlPlugin = new ObjectQLPlugin();
5666

@@ -81,11 +91,6 @@ export default defineStack({
8191
},
8292
new DriverPlugin(new InMemoryDriver(), 'memory'),
8393
new DriverPlugin(tursoDriver, 'turso'),
84-
new PackageServicePlugin(), // Package management service
85-
createTenantPlugin({ registerSystemObjects: true, registerLegacyTenantDatabase: false }),
86-
// Provisions the well-known system project (00000000-0000-0000-0000-000000000001)
87-
// on startup. Idempotent — safe to register unconditionally.
88-
createSystemProjectPlugin(),
8994
new AppPlugin(CrmApp),
9095
new AppPlugin(TodoApp),
9196
new AppPlugin(BiPluginManifest),

apps/server/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
"private": true,
77
"scripts": {
88
"dev": "objectstack serve --dev",
9-
"start": "objectstack serve",
9+
"dev:node": "tsx watch server/node-entry.ts",
10+
"start": "tsx server/node-entry.ts",
11+
"start:cli": "objectstack serve",
1012
"doctor": "objectstack doctor",
1113
"build": "objectstack compile",
1214
"typecheck": "tsc --noEmit",
@@ -22,6 +24,7 @@
2224
"@hono/node-server": "^1.19.14",
2325
"@libsql/client": "^0.17.2",
2426
"@objectstack/driver-memory": "workspace:*",
27+
"@objectstack/driver-sql": "workspace:*",
2528
"@objectstack/driver-turso": "workspace:*",
2629
"@objectstack/hono": "workspace:*",
2730
"@objectstack/metadata": "workspace:*",

0 commit comments

Comments
 (0)