Status: Accepted (v4 — supersedes v3)
Date: 2026-05-20 (v4)
Deciders: ObjectStack Protocol Architects
Supersedes: v1 (strict tree), v2 (siblings + sys_deployment join), v3 (siblings + deferred dev-workspace sys_project)
Builds on: ADR-0002 (Environment-Per-Database Isolation), ADR-0003 (Package as First-Class Citizen)
Consumers: @objectstack/service-tenant, @objectstack/service-cloud, @objectstack/spec/cloud, @objectstack/cli, the Console cloud_control app
v4 revision note — v3 kept
sys_projectreserved as a future dev-workspace concept (Phase 5 Builder UX). When we started drafting the Phase 5 schema we realised that the responsibilities we were about to hand tosys_projectare already covered bysys_package/sys_package_version/sys_package_installation(ADR-0003). Introducing a parallel "Project + Branch + Revision" stack would create two competing version-management trees in the control plane, exactly the drift this ADR family exists to eliminate.v4 therefore deletes the dev-workspace Project concept entirely. There is no
sys_project*table. There is nosys_environment_revisiontable in the target state — both are subsumed by the existing package three-tier (identity / immutable version / per-env installation).The local dev workspace continues to exist — but as local files + git, not as a server-side table. A
objectstack publishfrom any working copy is just "create a newsys_package_versionfor the implicit package that represents this code base".
After v3 we attempted to scope the Phase 5 dev-workspace schema and immediately hit overlap:
| Need | v3 plan (sys_project*) |
What already exists (sys_package*) |
|---|---|---|
| Code identity + namespace | sys_project (slug, owner_org) |
sys_package (manifest_id, owner_org, visibility) |
| Immutable version snapshots | sys_project_revision (commit_id, checksum, storage_key) |
sys_package_version (semver, checksum, storage_key, status) |
| Per-env deployment | (would have needed sys_deployment) |
sys_package_installation (env × package × version) |
| Cross-env promotion | (would have needed a join) | INSERT INTO sys_package_installation … SELECT … FROM staging-env |
| Marketplace distribution | n/a | visibility = 'marketplace', is_starter |
| Dependency declaration | n/a | sys_package_version.manifest_json deps |
Every Project responsibility maps 1:1 onto an existing Package row. The
only "missing" capability — git-style branches (parallel main / staging
/ feature heads) — can be expressed today with semver prereleases
(1.2.3-staging.4) and elevated to a real sys_package_channel mechanism
later when CI demands it.
Meanwhile the CLI publish path currently writes a third revision table
(sys_environment_revision, introduced transitionally during v3 cleanup).
That table holds nothing sys_package_version couldn't hold, but it
locks the runtime to a single-package-per-env assumption and prevents
Marketplace and user code from sharing one mental model.
The platform is still pre-launch; the same one-shot-wipe window v3 used remains open.
-
Drop the dev-workspace
sys_projectconcept. There is no Project table, no Project Branch table, no Project Revision table in the target state. The local working copy on a developer's machine is the "workspace" — versioned by git, not by a control-plane row. -
All code distribution flows through Package. Whether the source is a user-authored repo, a starter template, or a third-party marketplace submission, it ends up as a
sys_package(identity), one or moresys_package_versionrows (immutable snapshots), and zero-or-moresys_package_installationrows (per-env activation). -
sys_environment_revisionis transitional. Today the CLI publish path writes this table to keep things compiling. It will be removed once the CLI publish path is rewritten to call the package version create + installation upsert endpoints (Phase B below). -
Persona model collapses to one path. "Consumer" and "Builder" personas in v3 both ultimately install a
sys_package_versioninto asys_environment. The difference is which package they install (marketplace vs. their own), not which schema they touch.
Organization (account root — billing, members, SSO realm)
├── Environment (1..N — runtime container)
│ ├── hostname, database_url, plan, quota, status
│ └── installations → sys_package_installation
│ (env × package × version)
│
└── Package (0..N — code identity, Marketplace or private)
├── manifest_id, owner_org, visibility, is_starter
└── versions → sys_package_version
(immutable artifact snapshots, semver)
No FK between Environment and Package. They meet only at
sys_package_installation.
| Flow | What the user does | What writes to the DB |
|---|---|---|
| Install from Marketplace | Browse → pick package → choose env | INSERT sys_package_installation (env, package, version) |
objectstack publish (CLI) |
Local code → build → publish |
(a) ensure sys_package for this code base; (b) INSERT sys_package_version with new artifact; (c) upsert sys_package_installation pointing the env at the new version |
| Promote staging → prod | objectstack promote --from staging --to prod |
INSERT sys_package_installation for prod using staging's package_version_id (zero re-upload) |
| Rollback | objectstack rollback --env prod --to <ver> |
UPDATE sys_package_installation … SET package_version_id = <old> |
A user's repo is associated with a package the first time publish runs
against a given control plane. The package's manifest_id defaults to
local.<org_slug>.<project_slug> (overridable in objectstack.config.ts).
Subsequent publishes from the same repo create new sys_package_version
rows under that package.
This means user code, starter templates, and Marketplace apps are
indistinguishable to the runtime — they all resolve to a
sys_package_installation row at load time.
Today: encode the channel in the semver prerelease tag.
| Channel | Example version | Resolves on env via |
|---|---|---|
| stable | 1.4.0 |
package_version_id pin |
| staging | 1.4.0-staging.7 |
installation upgrade |
| PR preview | 1.4.0-pr.123.2 |
ephemeral env + installation |
Later: introduce sys_package_channel ({package_id, name, head_version_id})
plus sys_package_installation.tracking (pinned | channel_head) to
get CI-style "always run staging branch HEAD" without a Project table.
Unchanged from v3. The Cloudflare Worker resolves
<slug>.objectos.app → sys_environment.id. Runtime loads each
installed package's current version via sys_package_installation.
Unchanged surface area, simpler mental model:
Environments ← runtime targets
Packages ← code you own + Marketplace browser (one tab, filtered)
Members
Billing
There is no separate "Projects" tab. A power user inspecting "what's installed where" goes through Packages → version history → installations.
- One version-management spine. Marketplace apps, starter templates, and user code share the same three tables. Operators learn one model.
- Smaller schema. No
sys_project, nosys_project_branch, nosys_project_revision, nosys_environment_revision, nosys_deployment. The control plane keepssys_environment+sys_package*and nothing else for the deploy story. - Promote / rollback are SQL on
sys_package_installation. Same property v3 advertised, now also true for user code. - CLI publish and Marketplace install converge. A single set of permissions, audit events, and Studio screens covers both paths.
- Dev workspace = local files + git. We don't compete with git for branch / revision UX in the control plane.
- Two paths today don't converge yet. The CLI still writes
sys_environment_revision; until Phase B lands, "what version is running in prod" requires looking at two tables. Transitional. - No server-side branches today. Teams that want CI-driven
staging↔prodchannels must encode it in semver prereleases untilsys_package_channelships. @objectstack/spec/cloudAPI surface change.ProjectSchema,ProjectBranchSchema,ProjectRevisionSchemaare removed. No downstream consumers in production yet.
sys_package_version.published_from_project_id(introduced in v3) is removed as a field — provenance lives insys_package_version.metadataif needed.ProjectArtifactSchema(packages/spec/src/cloud/project-artifact.zod.ts) is the envelope returned byGET /cloud/projects/:id/artifact. The route name is kept for BC; the response shape continues to wrap the compiledObjectStackDefinitionSchema. The "Project" in the schema name is historical and will be renamed toEnvironmentArtifactin a follow-up.
| Phase | Scope | Status |
|---|---|---|
| A — Drop Project from the protocol | Remove packages/spec/src/cloud/project.zod.ts; update index.ts; trim Project tests from environment.test.ts; mark sys_environment_revision as @deprecated transitional |
✅ This commit |
| B — Rewire CLI publish onto Package | objectstack publish resolves implicit sys_package; calls POST /cloud/packages/:id/versions; upserts sys_package_installation. Old /cloud/projects/:envId/metadata becomes a thin BC shim that internally walks the new path. |
Next |
| C — Split CLI commands | objectstack push (version only) + objectstack deploy (installation upsert) + objectstack promote + objectstack rollback. publish stays as a push && deploy alias. |
Next+1 |
| D — Remove transitional revision table | After Phase B is shipping and verified: drop sys_environment_revision schema, delete _DEPRECATED route handlers, wipe table from any seeded control planes. |
After C |
E — sys_package_channel (optional) |
Only if real CI need surfaces. Adds named channels + tracking mode to installations. | Deferred |
There is no "Phase 5 Builder UX" anymore — Builder is just "you own private packages now", and the existing Packages UI covers it.
Pre-launch: drop tables, rebuild. Same one-shot wipe v3 used.
Post-launch (forward-looking, if v4 lands after launch):
- Read each
sys_environment_revisionrow. - Ensure an implicit
sys_packageexists for the env's owner org. - For each revision: insert
sys_package_version(semver =0.<idx>.0, storage_key copied verbatim, checksum copied verbatim). - Insert
sys_package_installationfor the env's current revision. - Drop
sys_environment_revision.
The script lives in @objectstack/service-tenant's migration folder
when needed.
manifest_iddefault for local code.local.<org_slug>.<project_slug>collides if two projects share a slug. Resolution: include a 6-hex suffix on first publish, persist it inobjectstack.config.ts.- Visibility default for CLI-created packages.
private(owner org only) — consistent withsys_package.visibilitydefault. Explicitobjectstack package publish --marketplaceflow to escalate. - Multi-package envs in Studio. The current "Environment detail" page assumes one artifact. Phase B+ updates it to list installations and show per-package versions.
- ADR-0002 — Environment-Per-Database Isolation
- ADR-0003 — Package as First-Class Citizen
- v3 (archived as the immediate predecessor):
0006-project-environment-split.md(this file's prior version, retained in git history) - Power Platform: Solution → Environment model (same shape, different names)
- Salesforce: Unlocked Package → Org model (no "Project" table either)
- npm + lockfile: identity + immutable version + installation pointer