Deployment Modes (Standalone & Cloud)
Run a single ObjectStack Server process in standalone (single-project) or cloud (multi-project) mode. Covers control-plane DB URLs, hostname routing, and Fly.io / Docker deployment.
Deployment Modes (Standalone & Cloud)
The same apps/objectos binary boots into one of two modes. Which one you get
depends only on the OS_MODE environment variable — the code and
Docker image are identical.
| Mode | OS_MODE | Control plane | Projects | Hostname routing | Typical use |
|---|---|---|---|---|---|
| standalone | unset / standalone | — (single kernel) | 1 | ❌ | OSS single-app install, laptop dev |
| cloud | cloud | OS_DATABASE_URL | 1 – N | ✅ | Studio dev, self-hosted multi-project, SaaS |
Legacy alias:
OS_MULTI_PROJECT=trueis still honoured as a deprecated alias forOS_MODE=cloud. The server prints a deprecation warning at boot and the variable will be removed in the next major release.
Both modes reuse:
KernelManager+DefaultProjectKernelFactoryfor per-project kernelsDefaultEnvironmentDriverRegistryfor hostname → project resolutioncreateControlPlanePlugins()(cloud only) to install tenant / auth / security / audit / package plugins onto the control DB.
Mode 1 — standalone
pnpm --filter @objectstack/objectos dev
# → http://localhost:3000One ObjectKernel, every plugin from apps/objectos/objectstack.config.ts, no
per-project isolation. This is what you want on a laptop or for an OSS
single-app install.
Leave OS_MODE unset (or set it to standalone) to use this mode.
Mode 1b — standalone from a pre-built artifact (no host config)
Since @objectstack/runtime@4.x the standalone path is built into the
framework: any directory containing a compiled dist/objectstack.json (or
reachable via OS_ARTIFACT_PATH, including an https:// URL) can be booted
with the stock CLI — no objectstack.config.ts required.
# Local artifact in cwd
cd hotcrm # https://github.com/objectstack-ai/hotcrm
objectstack start # auto-detects ./dist/objectstack.json
# Explicit artifact path
OS_ARTIFACT_PATH=/srv/apps/crm/objectstack.json objectstack start
# Remote artifact over HTTPS
OS_ARTIFACT_PATH=https://example.com/crm/objectstack.json objectstack startInternally the CLI calls createDefaultHostConfig() from
@objectstack/runtime, which wraps createStandaloneStack() and exposes the
artifact's requires / objects / manifest. This path is standalone-only
— it has no dependency on @objectstack/service-cloud and is the recommended
shape for shipping an app as a single JSON deployable.
For the multi-project / control-plane variant, keep using apps/objectos's
objectstack.config.ts (Mode 2 below) which composes cloud plugins on top.
Mode 2 — cloud
OS_MODE=cloud \
OS_DATABASE_URL=file:./.objectstack/data/control.db \
AUTH_SECRET=$(openssl rand -hex 32) \
pnpm --filter @objectstack/objectos dev- Control-plane tables (
sys_project,sys_project_credential, …) live in the database configured byOS_DATABASE_URL. Usefile:...for a local SQLite control plane, orlibsql://.../https://...for a remote Turso/libSQL control plane. - Projects are created through Studio /
POST /api/v1/cloud/projects. Each project binds its own driver (sqlite / libsql / turso / postgres). - Incoming requests are routed to per-project kernels by the
Hostheader (matched againstsys_project.hostname), or byX-Project-Id, or by the authenticated session'sactiveProjectId.
Configuring a project driver
When calling POST /api/v1/cloud/projects, pass driver as one of the
registered drivers: memory, sqlite, turso / libsql, or postgres.
The factory switch lives in
packages/runtime/src/project-kernel-factory.ts::createDriver.
For a remote shared control plane, only the URL changes:
OS_MODE=cloud \
OS_DATABASE_URL=libsql://control.turso.io \
OS_DATABASE_AUTH_TOKEN=... \
AUTH_SECRET=$(openssl rand -hex 32) \
pnpm --filter @objectstack/objectos devMultiple Server replicas can share the same remote sys_* schema;
DefaultEnvironmentDriverRegistry's per-replica cache is coherent within its
TTL (5 min default), and credential rotation fires
envRegistry.invalidate(projectId) so stale entries drop out within one TTL.
Hostname routing precedence
HttpDispatcher.resolveEnvironmentContext tries these signals in order and
stops at the first match:
/projects/:projectId/...URL segment.Hostheader →sys_project.hostname(viaenvRegistry.resolveByHostname).X-Project-Idheader.- Session
activeProjectId. - Session
activeOrganizationId→ that org's default project.
Control-plane routes (/auth, /cloud, /discovery, /health) skip
resolution and always hit the control-plane kernel.
Cross-subdomain session cookies
Host-based routing means Studio (say studio.example.com) and a project
subdomain (acme.example.com) are separate origins. Set
OS_COOKIE_DOMAIN=.example.com to opt in to better-auth's
cross-subdomain cookie mode. In production the session cookie is also scoped
secure automatically.
Deploying to Fly.io
ObjectStack ships a single Dockerfile (apps/objectos/Dockerfile) that works
for both Fly.io and bare Docker. The repo also ships a ready-to-use
apps/objectos/fly.toml.
# From the repo root
fly launch --copy-config --name objectstack-server \
--dockerfile apps/objectos/Dockerfile \
--no-deploy
# Persistent volume for the local-SQLite control DB
fly volumes create objectstack_data --size 3 --region iad
# Secrets (never commit!)
fly secrets set \
AUTH_SECRET=$(openssl rand -hex 32) \
OS_COOKIE_DOMAIN=.objectstack.app \
TURSO_ORG_NAME=... \
TURSO_API_TOKEN=...
fly deploy
# Wildcard cert for per-project subdomains
fly certs add "*.objectstack.app"
# Follow the DNS instructions Fly prints (_acme-challenge TXT + A/AAAA)Fly tuning
OS_KERNEL_CACHE_SIZE and OS_KERNEL_TTL_MS control the
per-project kernel LRU. The defaults in fly.toml (50 kernels, 30 min TTL)
fit a shared-cpu-2x / 1GB machine comfortably. Bump both proportionally if
you upgrade the VM.
Deploying with Docker (self-hosted)
docker build -f apps/objectos/Dockerfile -t objectstack/server:local .
docker run --rm -p 3000:3000 \
-v $(pwd)/data:/data \
-e OS_MODE=cloud \
-e OS_DATABASE_URL=file:/data/control.db \
-e AUTH_SECRET=$(openssl rand -hex 32) \
objectstack/server:local
curl http://localhost:3000/api/v1/healthPut a reverse proxy (Caddy / Traefik / Nginx) in front for wildcard TLS and
point a wildcard DNS (*.example.com) at it. The container does the rest.
Environment variable reference
| Variable | Mode(s) | Purpose |
|---|---|---|
OS_MODE | all | standalone (default) or cloud. Selects whether a control plane is mounted. |
OS_MULTI_PROJECT | all (deprecated) | Legacy alias: =true is treated as OS_MODE=cloud. Removed in next major. |
OS_DATABASE_URL | all | In standalone, the project business DB URL. In cloud, the control-plane DB URL (file:, libsql://, or https://). |
OS_DATABASE_AUTH_TOKEN | all | Auth token for OS_DATABASE_URL when using libSQL/Turso. |
TURSO_ORG_NAME + TURSO_API_TOKEN | cloud | Used by TursoProjectDatabaseAdapter to provision per-project databases. |
OS_KERNEL_CACHE_SIZE | cloud | LRU size for per-project kernels (default 32, fly.toml defaults to 50). |
OS_KERNEL_TTL_MS | cloud | Idle eviction TTL in ms (default 900000, fly.toml defaults to 1800000). |
OS_ENV_CACHE_TTL_MS | cloud | TTL for the envRegistry's hostname/id cache (default 300000). |
OS_COOKIE_DOMAIN | cloud | Shared cookie domain for cross-subdomain sessions (e.g. .objectstack.app). |
AUTH_SECRET | all | Better-Auth session secret (≥ 32 chars). |
PORT / HOST | all (node-entry) | HTTP bind. Defaults to 3000 / 0.0.0.0. In production a busy port is a hard error (no auto-shift) — pin it and match OS_AUTH_URL / OS_TRUSTED_ORIGINS. |
Smoke test (cloud mode)
# 1. Create a project
curl -X POST http://localhost:3000/api/v1/cloud/projects \
-H 'Content-Type: application/json' \
-d '{ "organization_id": "<org-id>", "display_name": "Demo", "driver": "sqlite" }'
# → { id: "<pid>", status: "provisioning", hostname: "<org-slug>-<short>.objectstack.app", ... }
# 2. Wait for status=active (Studio also polls this)
curl http://localhost:3000/api/v1/cloud/projects/<pid>
# 3. Call the project-scoped API via Host header
curl -H "Host: <hostname>" http://localhost:3000/api/v1/meta/objects
# → returns objects registered in the project's own sys_metadata, not the control planeIf step 3 returns control-plane objects instead of the project's own, the
host-based routing did not kick in — check that envRegistry is wired (see
apps/objectos/server/bootstrap.ts) and that sys_project.hostname was
populated.
Artifact publishing & versioning
Once a project exists, deploy metadata changes by compiling and publishing
artifacts rather than mutating tables directly. Each objectstack publish
creates an immutable revision in object storage; runtime nodes pull the
current (or pinned) commit via the artifact-api source.
See Publish, Versioning & Preview for the full loop —
storage backends (StorageServicePlugin local / S3), the
/api/v1/cloud/projects/:id/{revisions,artifact,activate} endpoints, and the
MetadataPlugin artifactSource: { mode: 'artifact-api' } runtime config.