ObjectStackObjectStack

18.0.0

The v18 GA release, published 2026-10-20: organization ownership is total (ADR-0131), so a database 17.x created takes the ownership ceremony before 18.0.0 serves it; protocol 18; a flow reads one expression dialect per slot; the security catalog is metadata; API keys expire; the CORS default admits no cross-origin caller. With its upgrade checklist, walked from 17.7.0.

Highlights — 18.0.0

  • A database 17.x created is upgraded by a ceremony before 18.0.0 serves it. ADR-0131 makes organization ownership total: no NULL organization_id. os serve 18.0.0 starts on a database it created itself, or on one that os migrate organization-ownership certified, and refuses any other before it listens (dd29bbe, 12d1476). The ceremony's plan reads only, and --apply runs only the plan you reviewed (f9b4217). ⚠️ Read step 2 of the upgrade checklist before the first 18.0.0 boot of a 17.x database.
  • Protocol 18. PROTOCOL_VERSION is 18.0.0: the handshake admits engines.protocol: '^18' and refuses '^17' (4e9fe9f). The ADR-0087 conversions whose one-major window ran out leave the load path, so a retired spelling is refused with its rename instead of being rewritten (5fd2c76, c5dd7fc, bc0538a, 105902f, 8693110, d176b0a, f606ba2, f901746).
  • A flow reads one expression dialect per slot (ADR-0032). A value slot takes a CEL value envelope and refuses the single-brace {…} template; a text slot takes {{ }} holes; a CEL root the flow does not bind is refused at os validate; $ names belong to the engine; and every builtin node's config is held to its executor's contract (0e9e7b7, 2b61f2d, 7958bdf, 5fb1746, 2f70c22).
  • Positions, permission sets and capabilities are metadata. The four catalog row objects retire (sys_position, sys_permission_set, sys_position_permission_set, sys_capability), a position names its sets in permissionSets, a grant names its set by name, and a name has one holder per deployment (ADR-0131 D3/D4; 66949b7, bfc15d2, 6f21216, fe98cc6). Declared sharing rules, webhooks and email templates are read from the registry and are read-only (5e97907, 191e043, 7de0265).
  • Page components are judged as errors. An undeclared prop, a value its row refuses or a missing binding fails os validate, os build, os lint and the metadata save door, where 17.7.0 printed an advisory (4f68ba1, 32fa194, d0c09e4). An element binds its data through dataSource only (317cddd).
  • Credentials and origins close by default. A key created without an expiry lives 90 days, a key unused for 90 days is refused, and a key that existed with no expiry has one year from the upgrade (1d79556). The CORS default admits no cross-origin caller, and '*' with credentials fails the boot (f7546a4). The inbound api hook verifies only the timestamped signature (5ce6773).
  • The AI skills ship with the packages, as @objectstack/skills, on the same version as every other @objectstack/* package.

Release facts, as measured

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

Measured
latest18.0.0, on every @objectstack/* package read (spec, cli, runtime, core, objectql, skills, organizations) and on create-objectstack
next18.0.0-next.5, which is older than latest. ⛔ Never install @next for 18: it resolves a prerelease that 18.0.0 supersedes
rc17.0.0-rc.6 (@objectstack/skills and @objectstack/organizations carry no rc tag)
Published@objectstack/cli@18.0.0 at 2026-10-20 12:44 UTC
Prereleases18.0.0-next.0 … next.3 and next.5. next.4 was never published, although the CHANGELOG carries a section for it

Install 18 by its exact version (18.0.0) or a caret range (^18.0.0); both resolve latest. Moving the stale next tag is a release act, outside these notes.

How to read the 18.0.0 CHANGELOG

The ## 18.0.0 section of each package's CHANGELOG.md is the whole v18 line: every changeset of every 18.0.0-next.N, in landing order. In @objectstack/spec it holds 341 entries; across the 53 packages whose CHANGELOG has an 18.0.0 section it holds 1,115 distinct entries, 340 of them marked breaking or narrowing. Read it with two cautions:

  • A later entry can undo an earlier one. In the CHANGELOG.md the published @objectstack/cli@18.0.0 tarball carries — the copy node_modules holds after you install 18.0.0 — ae6bd1a says "18.0 supports fresh single installs only — os migrate organization-ownership refuses, and os serve refuses a walled posture at boot"; 12d1476 and fc34d00, later in the same section, open the ceremony and lift the walled refusal, and the 18.0.0 code does what the later entries say. The repository's packages/cli/CHANGELOG.md no longer reads that way: #24912 amended the released ae6bd1a entry in place (it now reads "the 18.0 pre-release refusals of os migrate organization-ownership and of a walled posture at boot, both lifted in 18.0.0") and fc34d00's headline ("… supported on fresh installs and on databases the ownership ceremony upgraded"); the 18.0.0 tarball keeps the old text. record:json is declared by 97b8e7a and retired by 030a7b8; it shipped only in 18.0.0-next.3.
  • The refusal is the authority. Every refusal this page quotes was read off the published 18.0.0 packages, not off the CHANGELOG: where the two differ, the package's own message says what the package does.

The checklist below is curated from that record, not copied from it: it keeps the changes an application must edit for, and leaves out the ones that move only the platform's internals, its own tooling and the Console's code (the Console's changes are in the @objectstack/console CHANGELOG).


Upgrade checklist

⚠️ One checklist per release, for the release you are landing on and every release you cross to get there: a 17.4.0 app reads the 17.5.0, 17.6.0 and 17.7.0 lists before this one. On an 18.0.0-next.N prerelease, read the CHANGELOG entries after your prerelease instead: this list is written for 17.x.

18.0.0

How far this list has been walked. On 2026-10-20, from 17.7.0, twice: an app scaffolded with create-objectstack@17.7.0 (the blank template, pnpm, its skills installed by skills add) was booted once on 17.7.0 with objectstack start, so that it created a SQLite database holding one admin and three records (the second time also an API key and a comment with an emoji reaction), and was then moved to 18.0.0 from npm. The first pass drafted steps 1 and 2; the second, on a fresh scaffold, followed them as written, and both printed the numbers quoted below. Each line says how it was measured:

  • Measured in the walk — the step was run, and the quoted text is what 18.0.0 printed.
  • Probed on 18.0.0 — the walk's app carries none of it, so a probe stack was refused by the installed 18.0.0 packages (defineStack, or os validate exiting 1), and the quoted text is that refusal.
  • Read off the 18.0.0 tarball — the code or the message was found in the published package (npm pack @objectstack/PKG@18.0.0), and nothing was run.

⛔ The walk was one small app on SQLite, under the single posture, plus one upgrade of the same app's database created under group (step 4) and one of an os dev database (step 2). isolated databases, Postgres, MySQL and Turso were not walked, and no upgrade of a production application was run for this page.

1. Move the pins

  • Move every @objectstack/* dependency to 18.0.0 together, devDependencies included, and regenerate the lockfile — Moving the dependency pins. The walk moved the scaffold's seven ^17.7.0 ranges (@objectstack/cli among them) to ^18.0.0; pnpm install moved all seven to 18.0.0 and printed no peer-dependency warning. Measured in the walk.

  • Move the protocol declarations to 18. engines.protocol in your defineStack manifest becomes '^18', and specVersion in objectstack.manifest.json becomes '^18.0.0' — what create-objectstack@18.0.0 stamps. os migrate meta --from 17 --write rewrites engines.protocol for you (it printed objectstack.config.ts:38 manifest.engines.protocol: '^17' → '^18'); specVersion is a hand edit, and neither os validate nor the boot reads it. Left on '^17', os validate and os build still pass with a warning, and the boot refuses before it serves (exit 1):

    ⚠ Installed @objectstack/spec is v18.0.0 but this app declares engines.protocol '^17', which targets protocol v17.
    ✗ package 'com.example.v17app' targets protocol ^17 (engines.protocol) but this runtime is protocol 18.0.0. This is a major-version break. Run: objectstack migrate meta --from 17

    Measured in the walk.

  • Read os migrate meta --from 17 as a list to search, not a list to do. On the walk's app — one object, two fields, no flow, page or permission set — it printed 406 manual change(s) require your judgment: every protocol-18 manual entry it could not prove absent from the stack. Step 3 below is the curated version for an application. Measured in the walk.

  • Move the AI skills. From v18 the catalog is the @objectstack/skills package. A 17.x project has none: its skills were installed with skills add from the repository. Remove those, add the package at your @objectstack/spec version with your project's package manager, and copy it into your agent's directory:

    npx skills remove objectstack-ai objectstack-api objectstack-automation \
      objectstack-data objectstack-formula objectstack-i18n objectstack-platform \
      objectstack-pm-dispatch objectstack-query objectstack-ui objectstack-upgrade -y
    pnpm add -D "@objectstack/skills@$(node -p "require('@objectstack/spec/package.json').version")"
    #   npm: npm install --save-dev "@objectstack/skills@…"   ·   yarn: yarn add -D "@objectstack/skills@…"
    npx skills@1.7.2 experimental_sync --agent claude-code --copy -y

    npx skills list shows what is installed first — the walk listed the ten objectstack-* skills — and the removal deletes only the names it is given. Run before the removal, the sync skips every skill a skills add installed, exits 0, and leaves the 17.x-era skill text in place:

    !  Skipped objectstack-ai from @objectstack/skills: installed with `skills add` from objectstack-ai/objectstack
    …  (one line for each of the ten skills)
    —  Nothing to sync.

    After the removal it printed Synced 10 skills. In the walk's pnpm project npm install --save-dev exited 1 with npm error Cannot read properties of null (reading 'edgesOut'), so add the package with the manager that installed the project (create-objectstack uses pnpm when it is available). The command writes the exact version, "@objectstack/skills": "18.0.0": move it with the other pins at the next upgrade. Measured in the walk.

  • Run os validate, then os build. On the walk's app both passed on 18.0.0 once the declarations read '^18'. Step 3 lists what fails a richer app. Measured in the walk.

2. Upgrade the database

A database that 17.x (or an 18.0.0-next.N prerelease) created carries no organization-ownership marker, and 18.0.0 does not serve it until the ownership ceremony has certified it. A database 18.0.0 creates from empty writes its own birth marker and needs none of this. The overview is Upgrading to 18.0; the command's reference is os migrate organization-ownership; the lines below are the walk's measured output for each step.

  • Before the first 18.0.0 boot, clear the security-catalog overlays: os migrate security-catalog-overlays lists the environment's stored permission-set and position rows whose name a package now holds, which a v18 cold boot refuses with 422 NAMESPACE_CONFLICT; the preview exits 1 when it lists a row, and --apply deletes them (db9cf80, e030d43). On the walk's 17.7.0-created database it exited 0: ✓ No environment-wide permission-set or position row is stored over a package-held name — nothing here refuses the cold boot. Measured in the walk.

  • Start 18.0.0 against the database once. It is refused before it listens, exit 1, from the com.objectstack.organization-ownership-boot-guard plugin (error code ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED):

    ✗ This database carries no organization-ownership marker: 17.x or an 18.0 pre-release (18.0.0-next.*) created it, and this release serves only a database it created itself or one its ownership ceremony upgraded. sys_audit_log, sys_flow_credential, sys_flow_dispatch, sys_job, sys_job_queue, sys_job_run, sys_metadata, sys_metadata_audit, sys_metadata_commit, sys_metadata_history, sys_migration, sys_migration_journal, sys_secret still carry a physical organization_id column its object no longer declares. The server refuses to start rather than serve it in a shape it was never migrated to. Run 'os migrate organization-ownership --out PLAN' to write and review its plan, take a backup, then run 'os migrate organization-ownership --apply --plan-file PLAN --backup-taken', and start again.

    The refused boot is not wasted: its schema sync creates the tables 18.0.0 adds, which the ceremony moves rows into. In the walk, a plan drawn before that boot covered 77 tables and one drawn after it 81. Measured in the walk.

  • Do not reach for os migrate apply --allow-destructive first. Before the ceremony, the boot log and os migrate plan list the organization_id columns as orphaned and name os migrate apply --allow-destructive (28 destructive changes in the walk, with no word of the ceremony). Run on a copy of the walk's database, that apply exited 1 and dropped nothing: ✗ Refusing to drop sys_comment.reactions: the emoji reactions stored in it are not yet carried into sys_comment_reaction records …. The ceremony comes first. Measured in the walk, on a copy.

  • Take a backup. The platform takes none, and --apply asks you to state that you took one. For SQLite, stop the server and copy the database file with its -wal and -shm files, or use sqlite3 FILE ".backup …". Measured in the walk (a stopped-server copy).

  • Write the plan and review it: os migrate organization-ownership --out plan.json. It reads only; per table it prints the fate (attribution, column-drop, mirror-deletion, report), the row count, the rows with no organization, and whether the table will receive NOT NULL. The walk's plan: 81 tables, 34 to receive NOT NULL, unattributableRows: 0. A row whose owner cannot be derived blocks certification until you give it an organization or delete it. Measured in the walk.

  • Apply the reviewed plan: os migrate organization-ownership --apply --plan-file plan.json --backup-taken. The walk's run attributed 32 rows, rewrote 2 grants to name their permission set, deleted 49 mirror rows (the retired catalog rows and the seeded email templates), dropped 14 organization_id columns, set NOT NULL on 32 tables, and ended ✓ The ceremony is complete; recorded as adr-0131-organization-ownership. The walk ran it with no server running. Measured in the walk.

  • Start the server. In the walk 18.0.0 served the database: the 17.7.0 admin signed in with the same password and kept platform_admin, org_owner and admin_full_access; the three notes read back with their organization, and a fourth was written; an API key minted on 17.7.0 still answered 200. Measured in the walk.

  • Then clear the drift the ceremony leaves, and only that. The first served boot still logged three orphaned columns (sys_comment.reactions, sys_sso_provider.created_by / updated_by; on the group walk's database also the index idx_sys_sso_provider_organization_id). Carry the stored emoji reactions into sys_comment_reaction records first — os migrate comment-reactions to preview, then os migrate comment-reactions --apply --yes, which printed ✓ Converted 1 reaction(s); … the run is recorded as verified (9963322, 6673c80); without --yes it applies nothing and asks for it. Then run os migrate plan and read its destructive entries: ⛔ go on only if they are the ceremony's leftovers listed here. os migrate apply --allow-destructive applies every destructive change in the plan, drops and tightenings alike, so any other entry is your app's own — a column that may hold data — and is decided separately, not in this step. Then run os migrate apply --allow-destructive in a terminal: it prints the changes and asks Apply 4 change(s) to DATABASE? [y/N] (four on the group walk's copy); answer y only when the list is the one you checked (n printed ℹ Aborted — no changes made.). In a non-interactive shell it applies nothing, prints ⚠ Confirmation required. Re-run with --yes to apply, or use "os migrate plan" to preview. and still exits 0: read the output, not the exit code. The walk itself ran os migrate apply --allow-destructive --yes after that check, which dropped the three columns; the next boot logged no drift, and the reaction read back from sys_comment_reaction. Measured in the walk.

A development database (os dev). Measured on a 17.7.0 app whose database 17.7.0's os dev created: os serve --dev against that database file was refused with the ceremony named in its --dev form (Run 'os migrate organization-ownership --dev --out PLAN' …), and os dev 18.0.0 upgraded it in place — ✓ Upgraded the development database to the v18 organization ownership, with a copy kept under .objectstack/data/backups/ — then served it. ⚠️ Run os build first: while dist/objectstack.json was still the artifact 17.7.0's os start had compiled, the same os dev refused its plan with the protocol-^17 refusal and changed nothing (✗ The upgrade plan was refused, and nothing was changed.). Measured in the walk.

3. Edit what 18.0.0 refuses

Grouped by what you edit. Each line names the change in the 18.0.0 CHANGELOG (grep its hash) and what 18.0.0 answers when the old shape is still there. Most are refused at author time, so os validate finds them before a deploy: a schema refusal throws STACK_SCHEMA_INVALID (422) from defineStack, a cross-reference one STACK_CROSS_REFERENCE_INVALID (422), and an author-time rule fails os validate with exit 1. The metadata save door (Studio, /api/v1/meta, MCP) refuses the same shapes on a save: in the walk, PUT /api/v1/meta/page/NAME answered 422 INVALID_METADATA for an undeclared page:header prop and for kind: 'jsx'.

Flows: one expression dialect per slot
✗ flows.0.nodes.1.config.outputVariable: This `get_record` node's config is refused at `outputVariable` by the get_record contract: `$deal` is a `$` name, and the `$` names are reserved for the flow engine's own variables …
• flow 'pp_f1' · node 'u' (update_record) update_record field value at config.fields.owner: A value slot no longer reads the `{…}` template dialect: a string here is the literal text it spells, so a `{…}` token in it is refused rather than stored with its braces. …
• flow 'pp_f5' · node 'u' (notify) notify title at config.title: A flow text slot reads `{{ }}` template holes (ADR-0032 §3), not the single-brace `{…}` dialect …
• flow 'pp_f7' · node 'u' (update_record) update_record field value at config.fields.owner: `user` is not bound in a flow expression, so the run fails with `Unknown variable: user` …
  • Value slots take a CEL value envelope. The fields of create_record / update_record, assignment values, each operand of a CRUD node's filter, a notify node's recipients, sourceId and templateData values, a loop (with a body) or map collection, a subflow / map input, a script node's inputs, a screen node's defaults, defaultValue and recordId, and an http node's body. '{record.owner}' → { dialect: 'cel', source: 'record.owner' }; '{$User.Id}' → current_user.id (guard it with current_user != null in a flow that can run without a user); '{TODAY()}' → isoDate(today()), '{TODAY() + 7}' → isoDate(daysFromNow(7)), '{NOW()}' → isoDatetime(now()). CEL fails the run on an absent variable where the template wrote nothing: guard one that may be absent, has(vars.x) ? vars.x : null. A query placeholder in a CRUD filter ('{current_user_id}', '{today}') stays as it is. The refusal names the CEL spelling of each token (0e9e7b7, 6a06e3b, d85615d, 615c696, 111ffff, 9fdb127, f66fdc7, 12b9daf, 968acc8). Probed on 18.0.0 (os validate) at each of those positions but sourceId, templateData, defaults and recordId.
  • Text slots take {{ }} holes. A notify node's title, message and actionUrl, a screen node's title and description, a refusing end node's message, an http node's url and headers values, and the translated screen heading flows.FLOW.screens.NODE.title: {x} → {{ x }}. Compute anything else into a variable first, and a run-user value too — {{ $User.Id }} is refused (2b61f2d, 3073b72, 968acc8, d973c34). Probed on 18.0.0 (os validate) on notify title, message and actionUrl, screen title and http url.
  • CEL reads only the roots the flow binds. user.id, ctx.user.id, os.user.id → current_user.id. record is bound only on a flow an entrance hands a record (a record trigger, a type: 'flow' action, an inbound hook, …): elsewhere read the variable by its name or as vars.NAME. A condition is CEL: '{amount} > 100' → 'amount > 100' — the old spelling is refused as invalid CEL predicate (5fb1746, 0f77ff5, 243dd3c, 7958bdf). Probed on 18.0.0 (os validate).
  • No $ names of your own. An outputVariable, an errorVariable other than the default $error, an iteratorVariable / indexVariable, an idVariable, a screen field's name and a declared variable's name drop the $, and every read of them follows (3e72f93, 9f5eca5). Probed on 18.0.0 (defineStack) for outputVariable, errorVariable and iteratorVariable.
  • Node config is held to its executor's contract. An undeclared key, or a value of the wrong type, is refused at its location: fieldValues → fields; bulk / all / multiple → multi: true; a screen field's visibleIf → visibleWhen; a loop's itemVariable → iteratorVariable; delete an http node's outputVariable; a subflow config.timeoutMs moves onto the node; limit: '10' → limit: 10; a {token} in a literal key (limit: '{x}') → the literal (2f70c22, 78f841b, ac9f8bd, ca0bdbc). Probed on 18.0.0 (defineStack), each.
  • The retired aliases are refused with their rename. A CRUD node's config.object → objectName and config.filters → filter; a notify node's to / subject / body / url → recipients / title / message / actionUrl, and source: { object, id } → sourceObject / sourceId; map / subflow flow → flowName; script functionName / input → function / inputs; node types http_request, http_call and webhook → http; a wait node's loose config keys → waitEventConfig (duration → timerDuration, signal → signalName, timeoutMs → timerDuration as a string) and a connector_action node's → connectorConfig. os migrate meta --from 16 --write writes the ones it can prove (5fd2c76, f901746, d176b0a). Probed on 18.0.0 (defineStack), each but the nested source pair.
  • An edge names two nodes of its graph, once. An edge whose source or target is no node, or that repeats an earlier edge, is refused: delete it or re-point it (db4c45b). Probed on 18.0.0 (defineStack).
  • A retry block states its retries. A job retryPolicy with no maxRetries, or 0, is refused: write maxRetries: 3 (what such a block ran with before 17), or delete the block for a single attempt; backoffMultiplier defaults to 1, so state 2 to keep the old curve. A retry policy refuses a key it does not declare (retry.maxRetry) (f606ba2, ac8f2c5). Probed on 18.0.0 (defineStack).
Pages, views and actions
• page "pp_a14" · page:header: `bogusProp` is not a prop `page:header` declares (ComponentPropsMap, @objectstack/spec/ui): Unrecognized key(s) on this `page:header`: `bogusProp`. …
• page "pp_a3" · element:record_picker: dataSource.object: `element:record_picker` reads its records from the node-level `dataSource` binding only, and this node names no object there …
✗ pages.0.kind: Page `kind: 'jsx'` was removed in @objectstack/spec 18 (ADR-0087 D2) … Use `kind: 'html'`. …
✗ Object 'pp_deal' has two default record pages in `pages`: 'pp_r1' (stack.pages[0]) and 'pp_r2' (stack.pages[1]). Set `isDefault: false` on all but one of them. …
  • A page component's props are errors, not advisories. A prop its ComponentPropsMap row does not declare, a value the row refuses, a retired prop, or a missing data binding fails os validate, os build and os lint, and the metadata save door refuses the page (4f68ba1, 32fa194, d0c09e4). The component-props-* advisories you left from 17.7.0 now stop the build. Probed on 18.0.0 (os validate).
  • Elements bind data through dataSource. The flat object / filter / sort / limit of element:record_picker, element:number and element:repeater move to the node-level dataSource, a sibling of type; object-grid defaultFilters → filter where filter is empty, deleted where it is not (317cddd). Probed on 18.0.0 (os validate).
  • Renamed and retired page keys. kind: 'jsx' → 'html' (c5dd7fc); a page component's visibility and a form field's visibleOn → visibleWhen (105902f); page:header description → subtitle (8693110); delete page:header showStar (ae0ca66); delete a marketplace:installed-list component (14a8bd2). Probed on 18.0.0.
  • Page layout refusals. A main region takes only width: 'full' — delete width to keep the page as it rendered (3a734e0); a record page that authors both slots.details and slots.tabs is refused — move record:details into the first tab's children (e223152); at most one isDefault: true record page per object (5f37d36). Probed on 18.0.0 (defineStack).
  • List views. A type: 'chart' view binds chart: { dataset, values } (20e7d52); delete filterableFields — userFilters.fields declares a quick filter bar (4bc3bec); data: { provider: 'api' } is retired — bind the view to its object or static rows, and move an endpoint read onto a page component's dataSource (1912af4, 05ab9ef). Probed on 18.0.0 (defineStack).
  • List views are editable in place by default. A userActions block that leaves editInline out now parses to editInline: true: write userActions: { editInline: false } on a list that must stay read-only in place — a log, an audit trail (f53f14b). Probed on 18.0.0 (ListViewSchema).
  • A url target opens an address or an in-app path. A type: 'url' action's target, a page action row's url target, a dashboard header action's actionUrl and onSuccess.navigate take http://, https://, mailto: or a path starting / or ./; javascript:, data: and every other scheme are refused (9b37edb, f931e76). Probed on 18.0.0 (defineStack).
Objects, fields, formulas and validation rules
  • Retired object and index keys. Delete externalSharingModel (301eec6). A declared index's unique: true → unique: 'global' (the same index, nothing on disk changes) or unique: 'organization' (ef1fcb2). Probed on 18.0.0 (defineStack).
  • A controlled_by_parent object has exactly one required master_detail. Anything else is refused, as an object or as an objectExtensions entry; if the rows must outlive their parent, give the object its own sharingModel (76815b9, 4e0838d, ace0a53). Probed on 18.0.0 (defineStack).
  • A stage field's options declare their lifecycle. Every option of the field an object names as stageField carries kind: 'open' | 'won' | 'lost' (adad73f). Probed on 18.0.0 (defineStack).
  • Formulas and validation rules read one hop, and only declared fields. A second relationship hop (record.acct.parent.name) is refused — store the value one hop away; an unknown field is refused in every spelling (record['typo'], record.?typo), and through a reference a field the target does not declare (d537a31, 94379f4, 45bec43, 913fd6d). Probed on 18.0.0 (os validate).
  • A select option's visibleWhen reads what the server binds: record, previous and the acting user (current_user, and its aliases user, ctx, os). parent, ctx.locale, os.env and current_user.email are refused, and a gate the server cannot evaluate refuses the write instead of admitting the value (bb4f5cc, b1f7a7a, 46692c1, 440bed6). Probed on 18.0.0 (os validate).
  • A relationship does not target a stored-metadata table (sys_metadata and its siblings): store the item's type and name instead (3983965). Probed on 18.0.0 (os validate).
  • One name, one entry. Two entries with one name in one of 27 stack collections, and two functions with one name, are refused, naming both (1959aac, 147a0d1). defineSeed() refuses a record key that is no column of the object (9ad3d24). Probed on 18.0.0 for objects, functions and defineSeed.
Positions, permission sets and sharing rules
  • A position names its permission sets in permissionSets on its definition. A boot-time binder that wrote sys_position_permission_set rows has nothing left to write to: the four catalog row objects are retired (bfc15d2, 66949b7). Probed on 18.0.0 (defineStack accepts permissionSets); see the data line below for the rows.
  • Permission-set keys. Delete managedBy (bbdfc05) and adminScope.manageBindings / adminScope.authorEnvironmentSets (be48c5e); a body packageId that disagrees with the package shipping the set is refused — drop it (eeb95f7); a field-permission key with no object part ('.name') is refused — write 'OBJECT.FIELD' (30b0fc2). Probed on 18.0.0.
  • One holder per name. A package that registers a position, permission-set or capability name another package, the environment catalog or a built-in holds is refused 422 NAMESPACE_CONFLICT, naming both (fe98cc6, e030d43). Read off the 18.0.0 tarball (@objectstack/objectql).
  • Sharing rules. accessLevel: 'full' → 'edit' (5fd2c76). A declared rule (defineStack({ sharingRules })) is read from the registry and is read-only: sharing_rule is no longer written through /api/v1/meta, and no stored rule may take a declared name — clone it under a new name to customize it, and switch the declared one off through the activation ledger (1e8b628, 964a237, 5e97907). 'full' probed on 18.0.0; the rest read off the tarball (@objectstack/plugin-sharing).
  • A package manifest's permissions is the structured block { services, hooks, network, fs }; a flat list of strings is refused (fec87e7). Probed on 18.0.0 (defineStack).
Datasources, email templates, webhooks and inbound hooks
  • driver: 'mongo' → 'mongodb', and OS_DATABASE_DRIVER=mongo no longer selects a driver: os serve exits 1 with ✗ Unsupported driver "mongo" (OS_DATABASE_DRIVER / --database-driver). (bc0538a). Probed on 18.0.0 (defineStack, and os serve in the walk's app).
  • Email templates. Delete category and isSystem (ec7f7f8). Templates resolve from the registry: declare them in code — an organization can no longer create or edit a sys_email_template row, and the built-in seed is gone (7de0265, 8c5aa50). category probed on 18.0.0; the row refusal (an organization cannot … an email template row) read off the 18.0.0 tarball (@objectstack/plugin-email). The ceremony deleted the 24 seeded template rows in the walk.
  • Declared webhooks are read from the registry. No sys_webhook row is written for one, a sys_webhook row cannot take a declared name (409 RESOURCE_CONFLICT), and one is switched on and off through the activation ledger (191e043, fc34d00). Read off the 18.0.0 tarball (DeclaredWebhookNameConflictError, @objectstack/plugin-webhooks).
  • An inbound api hook verifies only the timestamped signature. Send x-objectstack-signature: t=UNIX_SECONDS,v1=HEX, the HMAC-SHA256 of UNIX_SECONDS.RAW_BODY under the flow's secret, within 300 seconds of the server clock; the body-only sha256=… form is refused (5ce6773). Read off the 18.0.0 tarball (signature-body-only, @objectstack/trigger-api).
Hooks and code against the published packages
  • objectstack build refuses some hook bodies it used to lower: one that reaches ctx.dispatch.scope or ctx.submitted — do the work at ctx.dispatch.index === 0 and read ctx.previous — and a parameter list it cannot rebind to the sandbox's ctx (1eff322, 6d36017). Read off the 18.0.0 tarball (@objectstack/cli, utils/extract-hook-body.js).
  • An in-process engine call carries a context: the caller's, or { isSystem: true }. A context with no principal that is not a system context is refused (a3bcbcf). Read off the 18.0.0 tarball (@objectstack/plugin-security).
  • Removed exports — each is a TS2305 after the upgrade: delete the import. normalizeVisibleWhen, VISIBILITY_ALIAS_KEYS and VISIBILITY_STRICT_OPTIONS (read visibleWhen; 2c702fd, ef1fcb2); CONVERSION_CONFLICT_CODE and the conversion-conflict channel (a65a59b); checkPageComponentViewData (a35f7a6); RunProvenanceContext (746637e); the zero-reader document schemas (326a90a); ISecurityService.discardPermissionSetOverlay (60bcfe7). Probed on 18.0.0 for the first five names (absent from the package).
  • Renamed registry key. A custom metadata type registration's allowOrgOverride → allowEnvironmentOverlay, also on GET /meta/types (e23eccb). Read off the 18.0.0 tarball (@objectstack/spec).
  • Hand-built kernels. I18nServicePlugin loses registerRoutes / basePath — the runtime dispatcher owns /api/v1/i18n (249130f); SecurityPlugin refuses a kernel that registers no sys_user / sys_member (c6fe02d). Read off the 18.0.0 tarball (@objectstack/service-i18n, @objectstack/plugin-security).
  • os meta resync is deleted (9e9e6f9): it answers Error: command meta:resync not found, exit 2. Measured in the walk.
  • Tests on @objectstack/verify. bootStack composes what objectstack serve composes from the same configuration, and boots the production single shape (7098aca, 97610a5, 34dba5a); runDashboardWindowProofs takes no options (06dbc4e); hooks.run(object, 'delete', …) resolves with the engine's answer (a6f3a13). Read off the 18.0.0 tarball (@objectstack/verify).
Clients of the REST API, the SDK and API keys

Every line here was answered by the walk's 18.0.0 server after the ceremony.

  • API keys expire. A key minted with no expires_at gets 90 days, and the response echoes the date; one asked for beyond a year is refused; a key unused for 90 days answers 403 API_KEY_INACTIVE; a revoked or expired key answers 403 API_KEY_REVOKED / API_KEY_EXPIRED where it used to answer 401. A key that existed with no expiry keeps working for one year from the first 18.0.0 boot, which stamps that date on it: rotate those keys within the year. The windows are the auth settings api_key_default_expiry_days, api_key_max_expiry_days and api_key_inactivity_days (1d79556). In the walk the 17.7.0 key still answered 200, with expires_at stamped one year after the first 18.0.0 boot; a new key got expires_at 90 days out; and a two-year key was refused:

    400 API_KEY_EXPIRY_TOO_LONG: expires_at 2028-10-20T00:00:00.000Z is further away than this deployment allows: an API key lives at most 365 days, so the latest expiry a key signed now may have is 2027-10-20T14:27:27.641Z. …

    Measured in the walk; the 403 codes read off the 18.0.0 tarball (@objectstack/core).

  • Name your cross-origin callers. With no origin list, the CORS answer admits no other origin: the walk's GET and preflight from https://app.example.com came back with no access-control-allow-origin. Set OS_CORS_ORIGIN=https://app.example.com (comma-separated for several), or the server plugin's cors.origins. A list holding '*' while credentials are on fails the boot: name the origins, or set OS_CORS_CREDENTIALS=false. A loopback origin is admitted on a development boot only. Same-origin requests, the Console included, need nothing (f7546a4). Measured in the walk.

  • No anonymous reads of the API description or the translations. GET /api/v1/openapi.json and GET /api/v1/i18n/locales answered 401 UNAUTHENTICATED with no session (166a94f, b53b949). Measured in the walk.

  • Request bodies are read as sent. A JSON body sent as text/plain answered 400 INVALID_REQUEST (The request body was sent as text/plain, which this server does not read into a request body.); a null or non-record body on a /data create answered 400 VALIDATION_FAILED, as did a PATCH whose expectedVersion was the number 3 — send the version you read as a string (76d9dca, 75ced06, 7c6b7f4, cac0be8, 1f3ac4f). Measured in the walk.

  • The catalog row objects are gone. GET /api/v1/data/sys_permission_set, sys_position, sys_position_permission_set and sys_capability answered 404; read positions and permission sets through /api/v1/meta/position and /api/v1/meta/permission. A grant row in sys_user_permission_set names its set in permission_set (the set's name), and carries no permission_set_id (66949b7, 6f21216, 0253192). Measured in the walk.

  • Upload scopes are a closed list: user, tenant, private, temp, attachments. scope: 'public' answered 400 INVALID_REQUEST (scope 'public' is not accepted: a storage scope never made a file publicly readable.): a file is served without sign-in only when its file record carries acl: 'public_read'. The storage settings' max_upload_mb is enforced (413 PAYLOAD_TOO_LARGE) (6a3fe25, ee8751d, 59d993c). Scope measured in the walk; PAYLOAD_TOO_LARGE read off the 18.0.0 tarball (@objectstack/service-storage).

  • Emoji reactions are records. sys_comment.reactions is retired: read and write sys_comment_reaction (9963322). Measured in the walk.

  • A public form's submit answers { id } only, not the stored record (7806a14). An import's runAutomations: false and treatAsHistorical: true are honoured only for the system principal and refused 403 PERMISSION_DENIED to any other caller (741aa85). A search over an object with no searchable field is refused 400 INVALID_FIELD instead of answering every row (ec749fb). Read off the 18.0.0 tarball (@objectstack/rest, @objectstack/objectql).

4. What 18.0.0 does not carry yet, and what it carries unlisted

The v18 line keeps landing on main, and the version commit did not take everything that landed before it. Read on main at 71696121a6: 26 pending changesets, ten of them minor, so the release that publishes them is 18.1.0. They fall in two sets.

Shipped in 18.0.0, listed in no 18.0.0 CHANGELOG. Eight of the 26 belong to commits that landed before the 18.0.0 version commit (06cab4849a), so their code is in the 18.0.0 packages while their entries will first appear in 18.1.0's CHANGELOG. Two of them reach an upgrade:

  • A production os serve boot with no auth secret is refused (18.x step 7, part 7b-3, #24845; it also stops an organization-less row drawing its autonumber from the __global__ counter). Measured on 18.0.0 — os serve exited 1 before it listened:

    ✗ This stack enables auth, but no auth secret is set, so AuthPlugin was not composed and no caller can authenticate — anonymous access to object data is always denied, so the data API would be unusable, and with no organization every row this boot writes would have no owner.

    Set OS_AUTH_SECRET to a long random value, identical across restarts and nodes (the walk set the older AUTH_SECRET, which 18.0.0 still reads); os dev uses a development fallback. os start is not refused: with no secret set it wrote HOME_DIR/auth-secret (mode 0600) on its first run and served sign-up. Measured on 18.0.0.

  • A declared sharing rule served under single carries deploymentWide: true where it carried organization_id: null, and the organization-less readers of sharing rules, approval units and the seed loader retire (18.x step 7, part 7a-3, #24828). Read off the 18.0.0 tarball (deploymentWide, @objectstack/plugin-sharing).

The other six are fixes with no edit for an upgrader, among them invite-member's refusal of a membership grade outside its vocabulary with code: 'ROLE_NOT_FOUND' (#24846, in the 18.0.0 @objectstack/plugin-auth).

Not in 18.0.0. Eighteen changesets belong to commits after the version commit, and only 18.1.0 carries them. Six are minor:

  • 18.x step 7, part 7a-2 (#24872, merged after the cut): a grant applies only in its own organization — an organization-less grant row confers nothing, and the Default Organization's admin_full_access grant is the only platform-admin anchor;
  • os migrate apply on an empty database writes the organization-ownership birth marker (#24875);
  • POST /api/v1/packages/:id/adopt-orphans refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED (#24861; absent from the 18.0.0 @objectstack/runtime);
  • on PostgreSQL, introspectSchema reads each table in its own schema (#24920); os lint reports a retired filterableFields by name (#24878); and a team record no longer lists Sessions (#24881).

The rest are fixes: masked password inputs in the identity action dialogs, an impersonation session that a target's forced password reset no longer holds, server.trustProxy read for the vendored auth library's client address, and others.

The walled postures on 18.0.0, measured. os serve 18.0.0 boots group and isolated on a fresh database (the 18.0.0-next.5 refusal of a walled posture is gone). Each prerequisite it names is refused at boot, in this order, until it is met:

✖ FATAL: tenancy posture 'group' was requested but @objectstack/organizations could not be loaded, so the organization wall is INACTIVE. Refusing to boot …
✗ [auth] tenancy posture 'group' requires OS_PLATFORM_OWNER_EMAIL to be set. …
✗ ✖ FATAL: tenancy posture 'group' is walled, but this deployment never declared a membership policy. …

With @objectstack/organizations@18.0.0 declared in the app, OS_PLATFORM_OWNER_EMAIL set, a membership policy declared (OS_AUTH_MEMBERSHIP_POLICY=invite-only) and a stable OS_SECRET_KEY, both postures printed ✓ Server is ready with Tenancy: group / Tenancy: isolated. Measured on a fresh database.

A walled 17.x database upgrades through the same ceremony. Measured on a 17.7.0 app booted under group with @objectstack/organizations@17.7.0 (its owner signed up and wrote two records): after the move to 18.0.0 — the exact "@objectstack/organizations": "17.7.0" pin moved by hand, like every @objectstack/* pin — os migrate security-catalog-overlays exited 0, the boot was refused as in step 2, the plan read posture group (81 tables, unattributableRows: 0), the apply completed, and 18.0.0 served the database under Tenancy: group with both records in their organization. Measured in the walk. An isolated database was not walked.

5. The npm tags

latest is 18.0.0; next still names 18.0.0-next.5, which is older — see Release facts. Pin 18.0.0 or ^18.0.0, and ⛔ never @next. Measured with npm view.

On this page