Skip to content
Merged
18 changes: 18 additions & 0 deletions .changeset/v18-ceremony-dev-databases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@objectstack/cli": minor
"@objectstack/runtime": minor
---

The v18 organization-ownership ceremony now completes on a long-lived 17.x database, and `os dev` runs it for you on a local development database (ADR-0131, amendment 2026-10-16).

**`os dev` upgrades a v17-shape development database in place.** When the boot refuses a local SQLite development database that is still in the v17 organization-ownership shape, `os dev` first copies the database and its `telemetry` sibling (with `-wal` / `-shm`) into `<data dir>/backups/organization-ownership-<timestamp>/`. It then runs `os migrate organization-ownership --dev` (the plan), applies that plan, and starts the server again. The plan, the apply report and the ceremony's log are kept beside the copy. If a step blocks, `os dev` stops and prints the blocker, where the copy is, and how to resume. `os serve` / `os start`, and any database that is not a local SQLite file, keep the manual ceremony and the boot refusal unchanged.

**What the ceremony decides for you is printed as a warning before it writes anything.** The plan carries a `warnings` list. The plan prints it, `--apply` prints it again before its first write, and the apply report keeps it.

**The `telemetry` sibling is covered.** A development boot (or an `OS_TELEMETRY_DB` deployment) keeps `sys_audit_log`, `sys_job_run`, `sys_notification` and the other lifecycle objects in a sibling database. The ceremony now plans, attributes and drops columns for each object in the database it lives in, reading anchors across databases. Before, the boot refusal read those tables in the sibling while the ceremony only read the primary, so an upgraded database could still never boot. A copy of such a table that an older boot left in the primary is listed under `staleCopies`, warned about, and left untouched; it no longer blocks the column drops.

**The shipped permission sets an install seeded before 2026-08-15 are adopted.** Those installs store the eight shipped sets (`organization_admin`, `member_default`, …) as `managed_by: 'admin'` rows. The authored-conversion step used to refuse them with "rename the authored row". The ceremony now re-owns them as the platform seeds they are and deletes them as mirrors once every grant names its set, so the shipped definitions apply. The warning names each row; a Setup edit to one of them is not carried over, and the pre-upgrade backup keeps it.

**`os migrate organization-ownership --dev`** composes the stack as `os serve --dev` does: the development auth secret's fallback (so the security and audit plugins), the development `.env*` files, and the `telemetry` sibling. A development boot's refusal now names the command with `--dev`, and every refusal now names an apply the ceremony accepts: `--out PLAN` first, then `--apply --plan-file PLAN --backup-taken`. A bare `--apply` is refused without a reviewed plan file. Before, running the ceremony from a shell without `NODE_ENV=development` composed neither plugin, and its apply blocked on the shipped permission sets and on `sys_audit_log`.

`OrganizationOwnershipBootGuardPlugin` (`@objectstack/runtime`) takes `{ dev }`; `organizationOwnershipRefusalMessage` and `assertOrganizationOwnershipMigrated` take the same option.
57 changes: 57 additions & 0 deletions content/docs/deployment/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,26 @@ os dev --database file:./data/test.db --auth-secret $(openssl rand -hex 32)
| `--seed-admin` / `--no-seed-admin` | — | Seed a dev admin (`admin@objectos.ai` / `admin123`) on an empty DB — default on; override with `--admin-email` / `--admin-password` |
| `-v, --verbose` | — | Verbose output |

##### A 17.x development database is upgraded in place

A v18 server refuses to start on a database still in the v17
organization-ownership shape until the [ownership ceremony](#os-migrate-organization-ownership)
has run on it. `os dev` runs that ceremony for you when the database is a local
SQLite file:

1. It copies the database file and its `telemetry` sibling (each with its
`-wal` / `-shm`) into `<data dir>/backups/organization-ownership-<timestamp>/`.
2. It runs `os migrate organization-ownership --dev` (the plan), then applies
that plan. The plan, the apply report and the ceremony's log are kept
beside the copy.
3. It prints the ceremony's warnings and starts the server again.

If a step blocks, `os dev` stops and prints the blocker, where the copy is, and
the command that resumes the run. It tries once per session. A database that is
not a local SQLite file (PostgreSQL, a remote libSQL, `:memory:`) is not
upgraded in place: run the ceremony yourself. `os serve` and `os start` never run
it; they refuse and name the command.

##### Serving dev over https

An interactive MCP client (and any OAuth client worth the name) refuses to open a
Expand Down Expand Up @@ -1172,6 +1192,7 @@ where the data lives.
| `os migrate value-shapes` | Scan stored reference and structured-JSON field values against the platform's value contract, and record the deployment's migration flag when clean |
| `os migrate summary-nulls` | Backfill roll-up `count` / `sum` columns still stored as `NULL` on parent rows created before the insert-time seed. Repairs values; no flag, nothing depends on it having run |
| `os migrate comment-reactions` | Carry the emoji reactions a 17.x deployment stored in the retired `sys_comment.reactions` column into `sys_comment_reaction` records, list every stored value that does not read, and record the verified run that lets `os migrate apply --allow-destructive` drop the column |
| `os migrate organization-ownership` | The v18 ownership ceremony (ADR-0131 D10): write a plan of what each table's rows will become, then `--apply` that reviewed plan. A v18 server refuses to start until it has run. See [below](#os-migrate-organization-ownership) |
| `os migrate activity-changed-fields` | Stamp the two read-rule columns (`changed_fields`, `milestone_key`) on the `sys_activity` rows written before they existed. Until it runs, those rows are withheld from every reader but the system, so run it at once after upgrading. No flag; idempotent |
| `os migrate meta --stored` | Replay the metadata conversion chain over this deployment's `sys_metadata` rows and rewrite the ones still carrying a pre-protocol shape. Hygiene, not a gate — nothing depends on it having run |
| `os migrate security-catalog-overlays` | List the environment-wide permission-set and position rows (the legacy plurals `permissions` / `positions` included) stored over a name a configured package holds — the rows a v18 cold boot refuses — each with the package that holds its name; `--apply` deletes them, one history tombstone per row. Run it before the first v18 boot, with the flags and environment the deployment boots with: the platform security plugin's sets are held only where `os serve` composes it (an auth secret set, and the `auth` tier on), and the step takes `serve`'s `--preset` and `--dev` with `serve`'s meaning. Adopts nothing; exits 1 while it lists a row |
Expand Down Expand Up @@ -1442,6 +1463,42 @@ the upgraded deployment's schema sync, which an `--apply` run performs itself:
a dry run against a database no upgraded process has opened cannot read them,
reports the failed read and exits `1`. A failed read or write exits `1`.

#### `os migrate organization-ownership`

The v18 ownership ceremony (ADR-0131 D10). A v18 `os serve` / `os start` refuses
to start on a database still in the v17 organization-ownership shape until this
has run. Under `os dev` it runs on its own for a local SQLite database (see
[`os dev`](#os-dev)).

```bash
os migrate organization-ownership --out plan.json # the plan: reads only
os migrate organization-ownership --apply --plan-file plan.json --backup-taken
os migrate organization-ownership --dev … # boot as `os serve --dev` does
```

- **The plan writes nothing.** Per table it lists the fate (column dropped,
mirror rows deleted, owner attributed, rows reported), the row counts, and the
rows whose owner cannot be derived. It refuses to plan a table it cannot read.
- **`--apply` runs only the plan you reviewed**, and only after you confirm a
backup (`--backup-taken`, or the prompt). The platform takes no backup. The run
is resumable: re-running it continues from its recorded checkpoint.
- **Warnings come before the first write.** The plan prints what the ceremony
decides on your behalf, and `--apply` prints the same warnings again before
it writes. An example: the shipped permission sets an install created before
2026-08-15 stored as `managed_by: 'admin'` are adopted as platform seeds and
deleted, and the shipped definitions apply. A Setup edit to one of those
sets is not carried over.
- **Every database the deployment uses.** A development boot, or one with
`OS_TELEMETRY_DB`, keeps the audit, job-run and notification objects in a
`telemetry` sibling database. The ceremony plans and applies each object in
the database it lives in. If an older boot left a copy of such a table in the
primary, the ceremony lists it under `staleCopies`, warns about it, and leaves
it alone.
- **Boot it the way the deployment boots.** `--dev` composes the stack as
`os serve --dev` does: the development auth secret's fallback (and with it
the security and audit plugins), the development `.env*` files, and the
`telemetry` sibling. Pass it for a database that `os dev` serves.

#### A database created by this version needs no migration

A deployment whose database the platform **creates from empty** records these
Expand Down
5 changes: 4 additions & 1 deletion content/docs/permissions/permission-sets.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,10 @@ posture, the platform operator. This has three author-visible consequences:
An older database keeps the `sys_permission_set` table and its rows, and
nothing serves or reads them. `os migrate organization-ownership --apply`
(ADR-0131 D10) converts the sets an organization authored in Setup into
environment metadata under `single` and deletes the seeded copies.
environment metadata under `single` and deletes the seeded copies. The shipped
sets that an install created before 2026-08-15 stored as `managed_by: 'admin'`
count as seeded copies too. The ceremony warns about each one before it writes
anything, and then the shipped definition applies.

A cross-package job function needs no hand-authored cross-package set: bind
each package's own sets to one **position** (the union model adds). Where a
Expand Down
Loading
Loading