|
| 1 | +# ADR-0006: Environment & Project — v4 (drop dev-workspace Project, unify on Package) |
| 2 | + |
| 3 | +**Status**: Accepted (v4 — supersedes v3) |
| 4 | +**Date**: 2026-05-20 (v4) |
| 5 | +**Deciders**: ObjectStack Protocol Architects |
| 6 | +**Supersedes**: v1 (strict tree), v2 (siblings + sys_deployment join), v3 (siblings + deferred dev-workspace `sys_project`) |
| 7 | +**Builds on**: ADR-0002 (Environment-Per-Database Isolation), ADR-0003 (Package as First-Class Citizen) |
| 8 | +**Consumers**: `@objectstack/service-tenant`, `@objectstack/service-cloud`, `@objectstack/spec/cloud`, `@objectstack/cli`, the Console `cloud_control` app |
| 9 | + |
| 10 | +> **v4 revision note** — v3 kept `sys_project` reserved as a future |
| 11 | +> *dev-workspace* concept (Phase 5 Builder UX). When we started drafting the |
| 12 | +> Phase 5 schema we realised that **the responsibilities we were about to |
| 13 | +> hand to `sys_project` are already covered by `sys_package` / |
| 14 | +> `sys_package_version` / `sys_package_installation`** (ADR-0003). |
| 15 | +> Introducing a parallel "Project + Branch + Revision" stack would create |
| 16 | +> two competing version-management trees in the control plane, exactly the |
| 17 | +> drift this ADR family exists to eliminate. |
| 18 | +> |
| 19 | +> v4 therefore deletes the dev-workspace Project concept entirely. There is |
| 20 | +> no `sys_project*` table. There is no `sys_environment_revision` table in |
| 21 | +> the target state — both are subsumed by the existing package three-tier |
| 22 | +> (identity / immutable version / per-env installation). |
| 23 | +> |
| 24 | +> The local dev workspace continues to exist — but as **local files + |
| 25 | +> git**, not as a server-side table. A `objectstack publish` from any |
| 26 | +> working copy is just "create a new `sys_package_version` for the implicit |
| 27 | +> package that represents this code base". |
| 28 | +
|
| 29 | +--- |
| 30 | + |
| 31 | +## Context |
| 32 | + |
| 33 | +After v3 we attempted to scope the Phase 5 dev-workspace schema and |
| 34 | +immediately hit overlap: |
| 35 | + |
| 36 | +| Need | v3 plan (`sys_project*`) | What already exists (`sys_package*`) | |
| 37 | +|:---|:---|:---| |
| 38 | +| Code identity + namespace | `sys_project` (slug, owner_org) | `sys_package` (manifest_id, owner_org, visibility) | |
| 39 | +| Immutable version snapshots | `sys_project_revision` (commit_id, checksum, storage_key) | `sys_package_version` (semver, checksum, storage_key, status) | |
| 40 | +| Per-env deployment | (would have needed `sys_deployment`) | `sys_package_installation` (env × package × version) | |
| 41 | +| Cross-env promotion | (would have needed a join) | `INSERT INTO sys_package_installation … SELECT … FROM staging-env` | |
| 42 | +| Marketplace distribution | n/a | `visibility = 'marketplace'`, `is_starter` | |
| 43 | +| Dependency declaration | n/a | `sys_package_version.manifest_json` deps | |
| 44 | + |
| 45 | +Every Project responsibility maps 1:1 onto an existing Package row. The |
| 46 | +only "missing" capability — git-style **branches** (parallel main / staging |
| 47 | +/ feature heads) — can be expressed today with semver prereleases |
| 48 | +(`1.2.3-staging.4`) and elevated to a real `sys_package_channel` mechanism |
| 49 | +later when CI demands it. |
| 50 | + |
| 51 | +Meanwhile the CLI publish path currently writes a **third** revision table |
| 52 | +(`sys_environment_revision`, introduced transitionally during v3 cleanup). |
| 53 | +That table holds nothing `sys_package_version` couldn't hold, but it |
| 54 | +locks the runtime to a single-package-per-env assumption and prevents |
| 55 | +Marketplace and user code from sharing one mental model. |
| 56 | + |
| 57 | +The platform is still pre-launch; the same one-shot-wipe window v3 used |
| 58 | +remains open. |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +## Decision |
| 63 | + |
| 64 | +1. **Drop the dev-workspace `sys_project` concept.** There is no Project |
| 65 | + table, no Project Branch table, no Project Revision table in the |
| 66 | + target state. The local working copy on a developer's machine is the |
| 67 | + "workspace" — versioned by git, not by a control-plane row. |
| 68 | + |
| 69 | +2. **All code distribution flows through Package.** Whether the source is |
| 70 | + a user-authored repo, a starter template, or a third-party marketplace |
| 71 | + submission, it ends up as a `sys_package` (identity), one or more |
| 72 | + `sys_package_version` rows (immutable snapshots), and zero-or-more |
| 73 | + `sys_package_installation` rows (per-env activation). |
| 74 | + |
| 75 | +3. **`sys_environment_revision` is transitional.** Today the CLI publish |
| 76 | + path writes this table to keep things compiling. It will be removed |
| 77 | + once the CLI publish path is rewritten to call the package version |
| 78 | + create + installation upsert endpoints (Phase B below). |
| 79 | + |
| 80 | +4. **Persona model collapses to one path.** "Consumer" and "Builder" |
| 81 | + personas in v3 both ultimately install a `sys_package_version` into a |
| 82 | + `sys_environment`. The difference is *which package* they install |
| 83 | + (marketplace vs. their own), not which schema they touch. |
| 84 | + |
| 85 | +``` |
| 86 | +Organization (account root — billing, members, SSO realm) |
| 87 | + ├── Environment (1..N — runtime container) |
| 88 | + │ ├── hostname, database_url, plan, quota, status |
| 89 | + │ └── installations → sys_package_installation |
| 90 | + │ (env × package × version) |
| 91 | + │ |
| 92 | + └── Package (0..N — code identity, Marketplace or private) |
| 93 | + ├── manifest_id, owner_org, visibility, is_starter |
| 94 | + └── versions → sys_package_version |
| 95 | + (immutable artifact snapshots, semver) |
| 96 | +``` |
| 97 | + |
| 98 | +**No FK between Environment and Package.** They meet only at |
| 99 | +`sys_package_installation`. |
| 100 | + |
| 101 | +### Two flows, one schema |
| 102 | + |
| 103 | +| Flow | What the user does | What writes to the DB | |
| 104 | +|:---|:---|:---| |
| 105 | +| **Install from Marketplace** | Browse → pick package → choose env | `INSERT sys_package_installation (env, package, version)` | |
| 106 | +| **`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 | |
| 107 | +| **Promote staging → prod** | `objectstack promote --from staging --to prod` | `INSERT sys_package_installation` for prod using staging's `package_version_id` (zero re-upload) | |
| 108 | +| **Rollback** | `objectstack rollback --env prod --to <ver>` | `UPDATE sys_package_installation … SET package_version_id = <old>` | |
| 109 | + |
| 110 | +### Implicit `sys_package` for user code |
| 111 | + |
| 112 | +A user's repo is associated with a package the first time `publish` runs |
| 113 | +against a given control plane. The package's `manifest_id` defaults to |
| 114 | +`local.<org_slug>.<project_slug>` (overridable in `objectstack.config.ts`). |
| 115 | +Subsequent publishes from the same repo create new `sys_package_version` |
| 116 | +rows under that package. |
| 117 | + |
| 118 | +This means user code, starter templates, and Marketplace apps are |
| 119 | +indistinguishable to the runtime — they all resolve to a |
| 120 | +`sys_package_installation` row at load time. |
| 121 | + |
| 122 | +### Branching strategy (interim) |
| 123 | + |
| 124 | +Today: encode the channel in the semver prerelease tag. |
| 125 | + |
| 126 | +| Channel | Example version | Resolves on env via | |
| 127 | +|:---|:---|:---| |
| 128 | +| stable | `1.4.0` | `package_version_id` pin | |
| 129 | +| staging | `1.4.0-staging.7` | installation upgrade | |
| 130 | +| PR preview | `1.4.0-pr.123.2` | ephemeral env + installation | |
| 131 | + |
| 132 | +Later: introduce `sys_package_channel` (`{package_id, name, head_version_id}`) |
| 133 | +plus `sys_package_installation.tracking` (`pinned` | `channel_head`) to |
| 134 | +get CI-style "always run staging branch HEAD" without a Project table. |
| 135 | + |
| 136 | +### Hostname routing |
| 137 | + |
| 138 | +Unchanged from v3. The Cloudflare Worker resolves |
| 139 | +`<slug>.objectos.app → sys_environment.id`. Runtime loads each |
| 140 | +installed package's current version via `sys_package_installation`. |
| 141 | + |
| 142 | +### Console UX |
| 143 | + |
| 144 | +Unchanged surface area, simpler mental model: |
| 145 | + |
| 146 | +``` |
| 147 | +Environments ← runtime targets |
| 148 | +Packages ← code you own + Marketplace browser (one tab, filtered) |
| 149 | +Members |
| 150 | +Billing |
| 151 | +``` |
| 152 | + |
| 153 | +There is no separate "Projects" tab. A power user inspecting "what's |
| 154 | +installed where" goes through Packages → version history → installations. |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +## Consequences |
| 159 | + |
| 160 | +### Positive |
| 161 | + |
| 162 | +1. **One version-management spine.** Marketplace apps, starter templates, |
| 163 | + and user code share the same three tables. Operators learn one model. |
| 164 | +2. **Smaller schema.** No `sys_project`, no `sys_project_branch`, no |
| 165 | + `sys_project_revision`, no `sys_environment_revision`, no |
| 166 | + `sys_deployment`. The control plane keeps `sys_environment` + |
| 167 | + `sys_package*` and nothing else for the deploy story. |
| 168 | +3. **Promote / rollback are SQL on `sys_package_installation`.** Same |
| 169 | + property v3 advertised, now also true for user code. |
| 170 | +4. **CLI publish and Marketplace install converge.** A single set of |
| 171 | + permissions, audit events, and Studio screens covers both paths. |
| 172 | +5. **Dev workspace = local files + git.** We don't compete with git for |
| 173 | + branch / revision UX in the control plane. |
| 174 | + |
| 175 | +### Negative / Costs |
| 176 | + |
| 177 | +1. **Two paths today don't converge yet.** The CLI still writes |
| 178 | + `sys_environment_revision`; until Phase B lands, "what version is |
| 179 | + running in prod" requires looking at two tables. Transitional. |
| 180 | +2. **No server-side branches today.** Teams that want CI-driven |
| 181 | + `staging` ↔ `prod` channels must encode it in semver prereleases |
| 182 | + until `sys_package_channel` ships. |
| 183 | +3. **`@objectstack/spec/cloud` API surface change.** `ProjectSchema`, |
| 184 | + `ProjectBranchSchema`, `ProjectRevisionSchema` are removed. No |
| 185 | + downstream consumers in production yet. |
| 186 | + |
| 187 | +### Neutral |
| 188 | + |
| 189 | +1. `sys_package_version.published_from_project_id` (introduced in v3) is |
| 190 | + removed as a field — provenance lives in `sys_package_version.metadata` |
| 191 | + if needed. |
| 192 | +2. `ProjectArtifactSchema` (`packages/spec/src/cloud/project-artifact.zod.ts`) |
| 193 | + is the envelope returned by `GET /cloud/projects/:id/artifact`. The |
| 194 | + route name is kept for BC; the response shape continues to wrap the |
| 195 | + compiled `ObjectStackDefinitionSchema`. The "Project" in the schema |
| 196 | + name is historical and will be renamed to `EnvironmentArtifact` in a |
| 197 | + follow-up. |
| 198 | + |
| 199 | +--- |
| 200 | + |
| 201 | +## Phasing |
| 202 | + |
| 203 | +| Phase | Scope | Status | |
| 204 | +|:---|:---|:---| |
| 205 | +| **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 | |
| 206 | +| **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 | |
| 207 | +| **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 | |
| 208 | +| **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 | |
| 209 | +| **E — `sys_package_channel` (optional)** | Only if real CI need surfaces. Adds named channels + tracking mode to installations. | Deferred | |
| 210 | + |
| 211 | +There is no "Phase 5 Builder UX" anymore — Builder is just "you own |
| 212 | +private packages now", and the existing Packages UI covers it. |
| 213 | + |
| 214 | +--- |
| 215 | + |
| 216 | +## Migration |
| 217 | + |
| 218 | +Pre-launch: drop tables, rebuild. Same one-shot wipe v3 used. |
| 219 | + |
| 220 | +Post-launch (forward-looking, if v4 lands after launch): |
| 221 | +- Read each `sys_environment_revision` row. |
| 222 | +- Ensure an implicit `sys_package` exists for the env's owner org. |
| 223 | +- For each revision: insert `sys_package_version` (semver = `0.<idx>.0`, |
| 224 | + storage_key copied verbatim, checksum copied verbatim). |
| 225 | +- Insert `sys_package_installation` for the env's *current* revision. |
| 226 | +- Drop `sys_environment_revision`. |
| 227 | + |
| 228 | +The script lives in `@objectstack/service-tenant`'s migration folder |
| 229 | +when needed. |
| 230 | + |
| 231 | +--- |
| 232 | + |
| 233 | +## Open questions |
| 234 | + |
| 235 | +1. **`manifest_id` default for local code.** `local.<org_slug>.<project_slug>` |
| 236 | + collides if two projects share a slug. Resolution: include a 6-hex |
| 237 | + suffix on first publish, persist it in `objectstack.config.ts`. |
| 238 | +2. **Visibility default for CLI-created packages.** `private` (owner org |
| 239 | + only) — consistent with `sys_package.visibility` default. Explicit |
| 240 | + `objectstack package publish --marketplace` flow to escalate. |
| 241 | +3. **Multi-package envs in Studio.** The current "Environment detail" page |
| 242 | + assumes one artifact. Phase B+ updates it to list installations and |
| 243 | + show per-package versions. |
| 244 | + |
| 245 | +--- |
| 246 | + |
| 247 | +## References |
| 248 | + |
| 249 | +- ADR-0002 — Environment-Per-Database Isolation |
| 250 | +- ADR-0003 — Package as First-Class Citizen |
| 251 | +- v3 (archived as the immediate predecessor): `0006-project-environment-split.md` (this file's prior version, retained in git history) |
| 252 | +- Power Platform: Solution → Environment model (same shape, different names) |
| 253 | +- Salesforce: Unlocked Package → Org model (no "Project" table either) |
| 254 | +- npm + lockfile: identity + immutable version + installation pointer |
0 commit comments