Publish, Versioning & Preview
End-to-end pipeline — `objectstack compile` → `objectstack publish` → cloud control plane stores immutable revisions in object storage → ObjectOS runtime pulls via artifact-api. Includes rollback and pinned-commit preview.
Publish, Versioning & Preview
The cloud control plane keeps every publish as an immutable revision in object storage and lets runtime nodes boot off any historical commit. This page walks through the full loop:
┌───────────┐ compile ┌──────────────────┐ publish ┌────────────────────┐ pull (artifact-api) ┌──────────────┐
│ source │ ───────────▶ │ dist/object- │ ──────────▶ │ Cloud control │ ──────────────────────▶ │ ObjectOS │
│ tree │ │ stack.json │ │ plane (revisions) │ │ runtime node │
└───────────┘ └──────────────────┘ └────────────────────┘ └──────────────┘
│
├── object storage
│ (file-storage / S3)
└── sys_project_revision
(commitId, checksum,
is_current, …)1. Compile
objectstack compile
# → dist/objectstack.json (envelope: { schemaVersion, commitId, metadata, … })The artifact is content-addressable: commitId is a sha256 prefix of the
canonicalised metadata. Identical content always produces the same commitId,
so re-publishing is a no-op upload.
2. Publish
objectstack publish dist/objectstack.json \
--project proj_crm \
--server http://localhost:4000Common flags:
| Flag | Env var | Default | Notes |
|---|---|---|---|
--server, -s | OS_CLOUD_URL | http://localhost:4000 | Control-plane URL |
--project, -p | OS_PROJECT_ID | — (required) | Target project id |
--token, -t | OS_CLOUD_API_KEY | — | Bearer token (when auth is enabled) |
--note, -n | — | — | Optional human-readable note attached to the revision (shown in Studio) |
--timeout | OS_CLOUD_TIMEOUT_MS | 60000 (60 s) | Set higher for slow networks; 0 disables |
publish POSTs to /api/v1/cloud/projects/:id/metadata. The control plane:
- Computes / verifies
commitId. - Uploads to object storage at
artifacts/orgs/${organizationId}/projects/${projectId}/${commitId}.json(no-op if the key already exists — content-addressable). Org-first prefixing makes per-tenant IAM policies, billing attribution, exports and GC trivial. Projects without anorganization_id(rare, single-tenant installs) fall back to the legacyartifacts/${projectId}/${commitId}.jsonshape. Existing rows keep their original key insys_project_revision.storage_key, so historical artifacts always remain readable regardless of layout changes. - Inserts a
sys_project_revisionrow, flips the previous current row tois_current = false, and marks the new oneis_current = true. - Updates
sys_project.metadata.current_commit_id.
3. Storage backend
The plugin resolves storage in this order:
- Explicit
options.storage.service(passed by host code) - Kernel-registered
file-storageservice (@objectstack/service-storage) - Local filesystem fallback (warns at boot)
createCloudStack() in @objectstack/service-cloud automatically wires
StorageServicePlugin based on environment variables — no code changes
needed when switching backends.
Env-driven configuration
| Env var | Required when | Description |
|---|---|---|
OS_STORAGE_ADAPTER | always (default local) | local | s3 | none (none skips registering, falls back to plugin's local-FS write — useful for tests) |
OS_STORAGE_LOCAL_DIR | local adapter | Root dir for local files (default ./storage) |
OS_S3_BUCKET | s3 adapter (required) | Bucket name |
OS_S3_REGION | s3 adapter (required) | AWS region (e.g. us-east-1, auto for R2) |
OS_S3_ENDPOINT | S3-compatible only | Custom endpoint (Cloudflare R2, MinIO, B2, etc.) |
OS_S3_ACCESS_KEY_ID | when not using SDK chain | Access key |
OS_S3_SECRET_ACCESS_KEY | when not using SDK chain | Secret key |
OS_S3_FORCE_PATH_STYLE | MinIO / self-hosted | 1 / true to force path-style URLs |
Example: native AWS S3
OS_STORAGE_ADAPTER=s3
OS_S3_BUCKET=objectstack-artifacts
OS_S3_REGION=us-east-1
OS_S3_ACCESS_KEY_ID=AKIA…
OS_S3_SECRET_ACCESS_KEY=…Example: Cloudflare R2
OS_STORAGE_ADAPTER=s3
OS_S3_BUCKET=objectstack-artifacts
OS_S3_REGION=auto
OS_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
OS_S3_ACCESS_KEY_ID=…
OS_S3_SECRET_ACCESS_KEY=…Example: MinIO (local development against S3 protocol)
OS_STORAGE_ADAPTER=s3
OS_S3_BUCKET=artifacts
OS_S3_REGION=us-east-1
OS_S3_ENDPOINT=http://localhost:9000
OS_S3_ACCESS_KEY_ID=minioadmin
OS_S3_SECRET_ACCESS_KEY=minioadmin
OS_S3_FORCE_PATH_STYLE=1Vercel (and other serverless hosts): the local-FS fallback does not work — Vercel functions get a read-only filesystem (only
/tmpis writable, and it is not shared across invocations). You must setOS_STORAGE_ADAPTER=s3plusOS_S3_*in your project settings. Pair it withOS_CONTROL_DATABASE_URLpointing at Turso / libSQL for the control DB.
4. Cloud HTTP API
All endpoints live under /api/v1/cloud/projects/:id/.
| Method & Path | Purpose |
|---|---|
POST /metadata | Publish a new revision (called by CLI) |
GET /artifact | Get the current artifact (envelope) |
GET /artifact?commit=<sha> | Get a pinned revision |
GET /revisions?limit=&cursor= | List revisions (most-recent first) |
POST /revisions/:commit/activate | Roll back / forward — flip is_current |
GET /resolve-hostname?host=<host> | Tenant routing helper used by ObjectOS |
Errors follow the standard { success: false, error: '…' } shape. Unknown
commits return 404 with a descriptive message.
5. Runtime — artifact-api source
ObjectOS runtime nodes can boot off any cloud revision without ever touching the source tree:
import { MetadataPlugin } from '@objectstack/metadata';
new MetadataPlugin({
bootstrap: 'artifact-only', // skip filesystem watcher
artifactSource: {
mode: 'artifact-api',
url: 'https://cloud.example.com',
projectId: 'proj_crm',
commitId: '9707264f0450e8ba', // optional — omit for latest
token: process.env.OS_CLOUD_API_KEY, // optional
fetchTimeoutMs: 60_000, // optional, default 60 s
},
});The plugin merges the cloud envelope’s metadata block, dynamically imports
@objectstack/spec/cloud for Zod validation, and registers every metadata
item before the kernel marks itself ready.
Tip — slow networks: override the timeout per-plugin (
fetchTimeoutMs) or globally viaOS_ARTIFACT_FETCH_TIMEOUT_MS.
6. Rollback
Every publish is preserved. The fastest way to roll back is the CLI:
# Accepts either the full 16-char commitId or any unambiguous prefix (≥ 8 chars).
OS_CLOUD_URL=http://localhost:4000 OS_PROJECT_ID=proj_crm \
objectstack rollback --commit 9ce1bd48Equivalent raw HTTP:
curl -X POST http://localhost:4000/api/v1/cloud/projects/proj_crm/revisions/9ce1bd48dd7022b8/activateThe next GET /artifact (no ?commit) — and any runtime node restarted
without a pinned commitId — will pick up the older revision. Roll forward
the same way.
Pruning old revisions
History grows with every publish. Use the prune endpoint to enforce a retention policy. The current revision is always preserved.
curl -X POST http://localhost:4000/api/v1/cloud/projects/proj_crm/revisions/prune \
-H 'Content-Type: application/json' \
-d '{"keepN": 50, "keepDays": 30}'Defaults are keepN = 50 and keepDays = 30. Any row outside both windows
is deleted from the table and the underlying object-storage key is
removed (best-effort — orphaned keys won't block the row deletion).
7. Local end-to-end smoke test
# Terminal 1 — cloud control plane
cd apps/cloud
OS_MODE=cloud PORT=4000 \
AUTH_SECRET=local-dev-secret-must-be-at-least-32-chars-long \
pnpm dev
# Terminal 2 — compile + publish twice
cd hotcrm # https://github.com/objectstack-ai/hotcrm
objectstack compile
OS_PROJECT_ID=proj_crm objectstack publish
# tweak any object label, recompile, publish again
# Terminal 3 — list revisions
curl http://localhost:4000/api/v1/cloud/projects/proj_crm/revisions | jqYou should see two sys_project_revision rows — the second one
isCurrent: true — and the artifact body for either one accessible via
GET /artifact?commit=<id>.
7. Public artifact API (visibility)
By default every project is private — only the authenticated /cloud/*
routes can read it. To support marketplace listings, OSS templates, share
links, or live documentation demos, switch a project to public or
unlisted.
// sys_project.visibility — defined in packages/platform-objects/src/tenant/sys-project.object.ts
visibility: 'private' | 'unlisted' | 'public' // default: 'private'| Mode | /pub/v1/.../artifact (no commit) | /pub/v1/.../artifact?commit=<id> | /pub/v1/.../revisions | /pub/v1/.../manifest.json |
|---|---|---|---|---|
private | 404 | 404 | 404 | 404 |
unlisted | 404 | ✅ 200 | 404 | 404 |
public | ✅ 200 (current) | ✅ 200 | ✅ 200 | ✅ 200 |
unlisted requires the caller to know the exact commit — it is meant for
share-preview links you mail to colleagues, not for crawling.
Public endpoints
GET /api/v1/pub/v1/projects/:id/artifact[?commit=<id>][&redirect=1]Returns the same envelope as the private route. No auth header required. SetsCache-Control: public, max-age=31536000, immutableandETag: "<commitId>"so a CDN (Cloudflare, CloudFront, …) in front of the control plane serves it from the edge for free. When the storage adapter is S3 / R2 (or anything that implementsgetSignedUrl), passing?redirect=1makes the control plane respond with a302to a short-lived (5 min) signed URL — bandwidth then comes straight from the bucket and the control plane is no longer the chokepoint.GET /api/v1/pub/v1/projects/:id/revisions?limit=N— public history.GET /api/v1/pub/v1/projects/:id/manifest.json— lightweight project info (projectId,displayName,currentCommitId,currentChecksum,builtAt).
Flipping visibility
# Via the platform REST (control plane)
curl -X PATCH https://cloud.example.com/api/v1/data/sys_project/proj_crm \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"visibility":"public"}'Or via SQL on the control DB:
UPDATE sys_project SET visibility='public' WHERE id='proj_crm';⚠️ Before going public, audit the artifact — it includes every field name, view config, AI prompt, and formula. Treat it like committing to a public Git repo: never embed credentials or PII in metadata.
Pulling a public artifact from another runtime
import { MetadataPlugin } from '@objectstack/metadata';
new MetadataPlugin({
artifactSource: {
mode: 'artifact-api',
url: 'https://cloud.objectstack.dev',
// No token needed for /pub routes
// Use the public path explicitly:
// url: 'https://cloud.objectstack.dev/api/v1/pub/v1/projects/proj_crm'
},
bootstrap: 'artifact-only',
});See also
packages/services/service-cloud— control-plane plugin sourcepackages/metadata—MetadataPluginand theartifact-apiloader- Cloud Deployment — running cloud / standalone modes