Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion content/docs/protocol/backward-compatibility.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ ObjectStack follows strict backward compatibility guarantees to ensure predictab

**The v18 line has exited prerelease mode.** While Changesets was in pre mode with the tag `next`, every release cut from `main` was the next `18.0.0-next.N` on npm's `next` tag, and `scripts/check-changeset-no-major.mjs` stood aside in pre mode, so a pull request could declare a major bump. Changesets has exited pre mode, so the line's GA version is `18.0.0`, which publishes to npm's `latest` tag, and the guard again fails a pull request that declares a `major` bump. See [how a breaking stage lands on the v18 line](/docs/releases/v18#how-a-breaking-stage-lands-on-this-line).

**After 18.0.0, a breaking change ships in a MINOR or a PATCH release.** The maintainer's 2026-10-21 ruling ([#25105](https://github.com/objectstack-ai/objectstack/issues/25105)) keeps the no-major guard and grades each change by the metadata protocol: a key, value or metadata type added, narrowed or given a new meaning takes MINOR, and every other change takes PATCH, breaking or not. A non-protocol breaking change (a REST response, a boot refusal, a CLI exit status, a removed runtime export) keeps its `**BREAKING**` banner and ships as a PATCH. 18.1.0, the first release under the rule, is a MINOR that carries 90 changesets declared breaking.
**After 18.0.0, a breaking change ships in a MINOR or a PATCH release.** The maintainer's 2026-10-21 ruling ([#25105](https://github.com/objectstack-ai/objectstack/issues/25105)) keeps the no-major guard and grades each change by the metadata protocol: a key, value or metadata type added, narrowed or given a new meaning takes MINOR, and every other change takes PATCH, breaking or not. A non-protocol breaking change (a REST response, a boot refusal, a CLI exit status, a removed runtime export) keeps its `**BREAKING**` banner and ships as a PATCH. 18.1.0, the first release under the rule, is a MINOR that carries 91 changesets declared breaking.
</Callout>

ObjectStack follows [Semantic Versioning 2.0.0](https://semver.org/) (`MAJOR.MINOR.PATCH`). The table below describes what each component means; which version a breaking change ships in depends on the line, as the callout above says.
Expand Down
2 changes: 1 addition & 1 deletion content/docs/releases/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ meaning takes `minor`, and every other change takes `patch` — a REST door, a
driver, a boot refusal, the CLI or the Console — breaking or not. A
non-protocol breaking change keeps its `**BREAKING**` banner and its ADR-0087
disposition. 18.1.0 is the first release cut under the rule: a minor carrying
90 changesets declared breaking, 42 of them graded `patch`.
91 changesets declared breaking, 43 of them graded `patch`.

What that means when you read a version number:

Expand Down
194 changes: 160 additions & 34 deletions content/docs/releases/v18/18-1.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,21 @@ title: 18.1.0
description: "Release notes and upgrade checklist for 18.1.0, the first minor of the v18 line: a minor that carries breaking changes. ADR-0131 finishes (an organization-less row confers nothing), a production os serve with no organization object is refused, nine metadata narrowings with automatic conversions, one federation rule at every datasource door, and an unreadable SQLite file is refused instead of replaced."
---

<Callout type="warn">
**How this page was compiled.** From the 18.1.0 changesets on `main` at
`ba9e0c3749` (2026-10-24), before the 18.1.0 cut: a rehearsed
`pnpm run version` in a throwaway clone at `b2c0752900` (73 packages move to
18.1.0, and their `## 18.1.0` sections held 227 distinct entries), and the 31
changesets merged after it, graded the same way. That makes **258 distinct
entries**. The five sections below use the grouping the GitHub Release body
uses, and the counts are that grouping's: **81** need you to act, **9** are
converted for you, **47** add capabilities, **3** declare a semantic change,
and **118** are other fixes. Nothing here was
read off a published 18.1.0 package or walked on an upgrade yet: every refusal
is described as its changeset describes it. Anything that lands on `main` before
the cut is in the CHANGELOG and not on this page.
<Callout type="info">
**How this page was compiled.** From the `## 18.1.0` sections of the published
CHANGELOGs (version commit `e649c2d91e`, #25606): 73 packages carry one, and
together they hold **265 distinct entries**. The five sections below use the
grouping the GitHub Release body uses, and the counts are that grouping's:
**82** need you to act, **9** are converted for you, **50** add capabilities,
**2** declare a semantic change, and **122** are other fixes. The
[upgrade checklist](#upgrade-checklist) was walked on 2026-10-24 from 18.0.0;
each line it measured says so. A refusal it did not reach is described as its
changeset describes it.
</Callout>

## Highlights — 18.1.0

- ⚠️ **A minor that breaks.** 18.1.0 is a minor version, and 90 of its
- ⚠️ **A minor that breaks.** 18.1.0 is a minor version, and 91 of its
changesets are declared breaking. Under the level rule the maintainer ruled
on 2026-10-21 (#25105, "ruling A"), the v18 line takes no further major: a
change to the metadata protocol takes `minor`, and every other change takes
Expand Down Expand Up @@ -72,6 +69,41 @@ the cut is in the CHANGELOG and not on this page.

---

## Release facts, as measured

Read with `npm view` and `npm pack` on 2026-10-24, after the publish:

| | Measured |
| :--- | :--- |
| `latest` | `18.1.0`, on every `@objectstack/*` package read (`spec`, `cli`, `runtime`, `core`, `objectql`, `skills`, `organizations`, `console`) and on `create-objectstack` |
| `next` | `18.0.0-next.5`, still **older** than `latest`. ⛔ Never install `@next` for 18 |
| `rc` | `17.0.0-rc.6` (`@objectstack/skills` and `@objectstack/organizations` carry no `rc` tag) |
| Published | `@objectstack/cli@18.1.0` at 2026-10-24 09:06 UTC; 73 GitHub Releases, one per package, tagged `<package>@18.1.0`, between 09:12 and 09:13 UTC |
| Version commit | `e649c2d91e` (#25606). The first 18.1.0 version commit, `74fa3cb6df` (#24876), was reverted before anything published (#25605) |

Read the 18.1.0 CHANGELOGs with two cautions:

- **The commit beside an entry is mostly not its own.** 260 of the 265 entries
open with `3937697`, the commit of #25605 that reverted the first cut and
re-added every pending changeset; only the five changesets merged after it
carry their own commit. The same is true in the published tarballs: all 34
entries of `@objectstack/cli@18.1.0`'s `## 18.1.0` section read `3937697`.
To find the change behind an entry, search its headline in the repository's
history; this page cites each entry's own commit.
- **A pinned `@objectstack/cli` still ranges.** The published CLI declares
eight of its `@objectstack/*` dependencies as caret ranges: `core`,
`driver-memory`, `driver-mongodb`, `driver-sql`, `driver-sqlite-wasm`,
`objectql`, `observability` and `runtime` (`^18.1.0` in 18.1.0, `^18.0.0` in
18.0.0). An app that pins every `@objectstack/*` package exactly therefore
gets a newer 18.x of those, and of the packages they depend on, as soon as one
publishes. Measured: an app pinned to exactly `18.0.0` installed today holds
17 `@objectstack/*` packages at 18.1.0 beside 60 at 18.0.0. See
[Move the pins](#1-move-the-pins) for the override that holds them.

The Docker image was not read for this page.

---

## What's new in 18.1.0

### The version number is not the signal
Expand All @@ -88,7 +120,7 @@ else:
| Everything else: REST, drivers, services, the CLI, the Console, `feat:` and `fix!:` alike | `patch` |

So a **patch** release can break a REST client, a boot or a deployment
script, and 18.1.0 carries 42 changesets graded `patch` that say **BREAKING**,
script, and 18.1.0 carries 43 changesets graded `patch` that say **BREAKING**,
beside 48 graded `minor` ones. Pin exact `@objectstack/*` versions, and read the
release notes for every version you cross. Each entry in a CHANGELOG states its
kind on its own `Metadata-protocol:` line, and the GitHub Release body groups the
Expand All @@ -98,11 +130,13 @@ From 18.1.0 the scaffolds follow the same advice: `npm create objectstack`,
`os init` and `os create` write the exact `@objectstack/*` version
(`18.1.0`, not `^18.1.0`), and `--in-repo` still writes `workspace:*`
(`975d5b3`, #25539). A project scaffolded earlier keeps its caret ranges until
you change them.
you change them. Measured: `create-objectstack@18.1.0` wrote `"18.1.0"` for all
nine `@objectstack/*` entries, and `create-objectstack@18.0.0`, run on
2026-10-24, wrote `^18.0.0` and installed 18.1.0.

### Breaking changes you must act on

The 81 entries that are breaking and have no ADR-0087 conversion, and one boot
The 82 entries that are breaking and have no ADR-0087 conversion, and one boot
refusal (`db1c7cc`) that its changeset declares as an addition. Each line names
the one fix; the CHANGELOG entry carries the full FROM → TO.

Expand Down Expand Up @@ -231,7 +265,11 @@ door (`422 INVALID_METADATA`), where 18.0.0 accepted it.
params, a single-brace `{x}`, logic or an unknown formatter inside a hole, or
a blank `''`. 18.0.0 warned. Fix: write holes over `count` or the params,
double a literal single brace, and delete a blank `confirmText`. A view
already stored still loads.
already stored still loads. *Probed on 18.1.0:* a view saved with
`confirmText: 'Retitle {x} notes?'` answered `422 INVALID_METADATA` with
`` A bulk action's `confirmText` reads `{{ }}` template holes (ADR-0032 §3),
not the single-brace `{…}` dialect `` and named `count` and the param as the
holes it binds; `'Retitle {{ count }} notes?'` saved.
- **A row policy that reads `x in current_user.KEY` which nothing owns, at the
permission save door** (`b531016`, #25550). It answered 200 and matched no
rows; it is now `422 INVALID_METADATA`. An object write is now judged by the
Expand Down Expand Up @@ -310,10 +348,20 @@ door (`422 INVALID_METADATA`), where 18.0.0 accepted it.
refused by every data verb (`8010b64`, `72fad92`, `a1e51d6`, `fb7501b`,
`882372d`). Fix: nothing if the declarations say what you mean; otherwise set
`enable.apiEnabled`, or re-enable the package.
- **Reassigning an approval to an address that resolves to no one is
refused** (`a57643d`, #25592): `POST /api/v1/approvals/requests/:id/reassign`
and `ApprovalService.reassign` answer `400 VALIDATION_FAILED` naming the
address, where a `to` nobody holds was written onto the slate and the request
waited on no one. A user id or email must belong to a member of the request's
organization, and `position:<name>` must have a holder there; `team:`,
`department:` and other addresses are always refused. Fix: reassign to a
person in the organization, or to a held position.
- **`POST /api/v1/meta/:type/:name/rollback` takes `toVersion` as a JSON whole
number of at least 1** (`0fab7ba`, #25532), and anything else answers
`400 INVALID_REQUEST`. The query form `?toVersion=3` still works. Fix: send
`{"toVersion": 2}`, not `"2"` or `2.5`.
`{"toVersion": 2}`, not `"2"` or `2.5`. *Measured in the walk:* `{"toVersion":"1"}`
and `{"toVersion":1.5}` answered `400` with `'toVersion' (positive integer) is
required`, and `{"toVersion":1}` rolled the view back.
- **The Data API refuses a malformed or negative page window with `400`**
naming the parameter (`17f3c65`, #25260), and the published list-query
schemas declare it a non-negative integer (`5f30f00`, #25300). Fix: send a
Expand Down Expand Up @@ -498,14 +546,25 @@ rows.
`controlled_by_parent` detail follows its master. Nothing changes for a
principal that does not hold the `external` grade, and no earlier release
shipped it.
- **A share or sharing rule on a public object reaches an external member**
(`4a25192`, #25591). On a `public_read_write` object, or a system object
with no `sharingModel`, `SharingService.grant` now admits a `user` recipient
who is an `external` member of the record's organization, and a sharing
rule writes rows for the external members it expands to and skips the
internal ones. Every other recipient keeps `422 SHARING_NOT_ENABLED`. The
lint's `sharing-rule-object-not-shareable` on a public object is now a
warning.
- **`FlowSchema.runAs` now says what a `system` run may not write**
(`025f6de`, #25564): its description names the refused writes to the four
identity-assignment objects (above, under security), and ADR-0087 D3 records
the change of meaning as `flow-run-as-system-identity-assignment-write-refused`,
so `os migrate meta` lists it for a flow that wrote those rows. It carries no
conversion, and every flow parses as before.

A change of meaning that also breaks is listed with the breaking changes above.
#25541 and #25591 declare `Metadata-protocol: semantic` too; the GitHub Release
body files them under new capabilities, because each widens what a door
admits, and the counts above follow it. A change of meaning that also breaks
is listed with the breaking changes above.

### Notable fixes in 18.1.0

Expand Down Expand Up @@ -540,7 +599,9 @@ A change of meaning that also breaks is listed with the breaking changes above.
#25370), and the scaffolds ship a starter test so a new project's `npm test`
passes (`6bc8cc6`, #25236; `9c431ba`, #25196). Saving the mail provider
"None (log only)" stops delivery, where the SMTP transport the boot built kept
sending (`0ec4c5a`, #25497).
sending (`0ec4c5a`, #25497). `POST /api/v1/data/:object/import` reads a
dotted (`2011.07.01`) or compact (`20090615`) year-first day, and matches a
select cell's option value in any case (`9f39094`, #25596).

The other fixes are in the per-package CHANGELOGs.

Expand Down Expand Up @@ -599,26 +660,74 @@ also requires.
### 18.1.0

<Callout type="warn">
**Not walked yet.** This list is compiled from the changesets before the cut.
No app has been moved from 18.0.0 to 18.1.0 for it, and no refusal below was
read off a published package.
**How far this list has been walked.** On 2026-10-24, from 18.0.0, once: an app
scaffolded with `create-objectstack@18.0.0` (the `blank` template, pnpm, no
skills), pinned to exactly `18.0.0` with the override in step 1, was booted on
18.0.0 with `objectstack start` and `OS_AUTH_SECRET` set. It created a SQLite
database holding one administrator, signed up through the auth API, and three
notes. It was then moved to 18.1.0 from npm, and each step below was run. Each
line says how it was measured:

- **Measured in the walk** — the step was run, and the quoted text is what
18.1.0 printed.
- **Probed on 18.1.0** — the walk's app carries none of it, so a probe was sent
to the running 18.1.0 server, and the quoted text is its answer.

⛔ The walk was one small app on SQLite, under the `single` posture. Walled
postures, Postgres, MySQL, Turso, a multi-environment host, an embedded kernel,
a flow, a webhook and the skills sync were not walked, and no upgrade of a
production application was run for this page. A line with no mark is compiled
from the changesets.
</Callout>

#### 1. Move the pins

Move every `@objectstack/*` dependency to `18.1.0` together, and
`@objectstack/skills` with them, as in the
[18.0.0 walk](/docs/releases/v18/18-0#1-move-the-pins). Pin the exact version:
a caret range takes every later 18.x by itself, and a patch can break a
non-protocol surface. A project `npm create objectstack` scaffolded before
18.1.0 carries caret ranges: replace each `^18.x.y` with `18.1.0` and
reinstall.
- **Move every `@objectstack/*` dependency to `18.1.0` together**, and
`@objectstack/skills` with them, as in the
[18.0.0 walk](/docs/releases/v18/18-0#1-move-the-pins). Pin the exact
version: a caret range takes every later 18.x by itself, and a patch can
break a non-protocol surface. A project `npm create objectstack` scaffolded
before 18.1.0 carries caret ranges: replace each `^18.x.y` with `18.1.0` and
reinstall. The walk moved nine exact `18.0.0` pins to `18.1.0`, and
`pnpm install` left no `@objectstack/*` package at 18.0.0 in the lockfile and
printed no peer-dependency warning. *Measured in the walk.*
- **Hold the CLI's own ranges with an override.** An exact pin does not hold
the eight packages `@objectstack/cli` ranges (see
[Release facts](#release-facts-as-measured)). With pnpm, add this to
`pnpm-workspace.yaml`, at the version you pin:

```yaml
overrides:
'@objectstack/core': '18.1.0'
'@objectstack/driver-memory': '18.1.0'
'@objectstack/driver-mongodb': '18.1.0'
'@objectstack/driver-sql': '18.1.0'
'@objectstack/driver-sqlite-wasm': '18.1.0'
'@objectstack/objectql': '18.1.0'
'@objectstack/observability': '18.1.0'
'@objectstack/runtime': '18.1.0'
```

On the walk's app pinned to `18.0.0`, those eight lines (at `'18.0.0'`) took
the tree from 17 packages at 18.1.0 to none. pnpm 10.28 ignored the same
block under `"pnpm": { "overrides": … }` in `package.json` while
`pnpm-workspace.yaml` exists. npm's `overrides` was not tried. Move the
override with the pins at every upgrade. *Measured in the walk.*

#### 2. Before the first boot

- Production `os serve` or `os start`: set `OS_AUTH_SECRET`, and do not rely on
- **Production `os serve`: set `OS_AUTH_SECRET`**, and do not rely on
`--no-server` or `OS_MIGRATE_AND_EXIT=1` to boot without it. A kernel you
embed needs it too, unless it boots with `NODE_ENV=development` or `test`.
Without it, `os serve` exited `1` with
`✗ This stack enables auth, but no auth secret is set, so AuthPlugin was not
composed and no caller can authenticate` and the line to set it.
*Measured in the walk.*
- **`os start` does not refuse: it mints a secret.** With no secret set, it
wrote a new one to `.objectstack/auth-secret` and booted. Boot with the
secret 18.0.0 used: with the same `OS_AUTH_SECRET`, the session signed in on
18.0.0 was still valid on 18.1.0; a new secret ends every session.
*Measured in the walk.*
- A stack that mounts no auth (`auth` tier off, `--preset minimal`) fails a
production boot: enable auth.
- CORS: list every cross-origin front-end in `OS_CORS_ORIGIN`, loopback origins
Expand All @@ -636,6 +745,13 @@ os build
os migrate meta --from 17 # the step-18 conversions, listed for your sources
```

On the walk's app, `os validate` and `os build` passed on 18.1.0 with no
edit. `os migrate meta --from 17` exited `0` and printed
`417 manual change(s) require your judgment` (18.0.0 printed 406): read it as a
list to search, as on 18.0.0. `--from 18` replays nothing, and says so
(`No migration step exists for protocol 18 → 18`), so keep `--from 17`.
*Measured in the walk.*

Fix each refusal with the line in
[Breaking changes you must act on](#breaking-changes-you-must-act-on) and
[Converted for you](#converted-for-you-by-os-migrate-meta). The ones an app is
Expand All @@ -647,11 +763,21 @@ managed datasource.

#### 4. After the boot

- The data is as 18.0.0 left it. The walk's three notes read back, and the
administrator signed in. A clean boot printed no upgrade report at the
default log level. *Measured in the walk.*
- Run `os migrate meta --stored` to list stored rows the step-18 conversions
rewrite.
rewrite. It takes no `--from` (`--stored=true cannot also be provided when
using --from`); on the walk's app, which had stored no metadata, it printed
`Examined 0 stored metadata row(s)` and a warning that an empty run attests
nothing. *Measured in the walk.*
- Run `os migrate plan`. On a database 18.0.0 created, it lists the tenant
columns still nullable; the ownership ceremony tightens them to `NOT NULL`
when you choose to run it.
when you choose to run it. On the walk's database it printed
`33 tenant column(s) are still nullable (ADR-0131 D1)`, said the ceremony
tightens 31 of them, and ended
`✓ Physical schema is in sync with metadata — nothing to migrate.`
*Measured in the walk.*
- If the boot reports a notification preference collision, resolve it with
`os migrate duplicates` and restart.
- Check that REST and MCP callers do not depend on a disabled package's actions
Expand Down
Loading
Loading