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 serve18.0.0 starts on a database it created itself, or on one thatos migrate organization-ownershipcertified, and refuses any other before it listens (dd29bbe,12d1476). The ceremony's plan reads only, and--applyruns 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_VERSIONis18.0.0: the handshake admitsengines.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 atos validate;$names belong to the engine; and every builtin node'sconfigis 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 inpermissionSets, 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 lintand the metadata save door, where 17.7.0 printed an advisory (4f68ba1,32fa194,d0c09e4). An element binds its data throughdataSourceonly (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 inboundapihook 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 | |
|---|---|
latest | 18.0.0, on every @objectstack/* package read (spec, cli, runtime, core, objectql, skills, organizations) and on create-objectstack |
next | 18.0.0-next.5, which is older than latest. ⛔ Never install @next for 18: it resolves a prerelease that 18.0.0 supersedes |
rc | 17.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 |
| Prereleases | 18.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.mdthe published@objectstack/cli@18.0.0tarball carries — the copynode_modulesholds after you install 18.0.0 —ae6bd1asays "18.0 supports freshsingleinstalls only —os migrate organization-ownershiprefuses, andos serverefuses a walled posture at boot";12d1476andfc34d00, 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'spackages/cli/CHANGELOG.mdno longer reads that way: #24912 amended the releasedae6bd1aentry in place (it now reads "the 18.0 pre-release refusals ofos migrate organization-ownershipand of a walled posture at boot, both lifted in 18.0.0") andfc34d00's headline ("… supported on fresh installs and on databases the ownership ceremony upgraded"); the 18.0.0 tarball keeps the old text.record:jsonis declared by97b8e7aand retired by030a7b8; it shipped only in18.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, oros validateexiting1), 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,devDependenciesincluded, and regenerate the lockfile — Moving the dependency pins. The walk moved the scaffold's seven^17.7.0ranges (@objectstack/cliamong them) to^18.0.0;pnpm installmoved all seven to 18.0.0 and printed no peer-dependency warning. Measured in the walk. -
Move the protocol declarations to 18.
engines.protocolin yourdefineStackmanifest becomes'^18', andspecVersioninobjectstack.manifest.jsonbecomes'^18.0.0'— whatcreate-objectstack@18.0.0stamps.os migrate meta --from 17 --writerewritesengines.protocolfor you (it printedobjectstack.config.ts:38 manifest.engines.protocol: '^17' → '^18');specVersionis a hand edit, and neitheros validatenor the boot reads it. Left on'^17',os validateandos buildstill pass with a warning, and the boot refuses before it serves (exit1):⚠ 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 17Measured in the walk.
-
Read
os migrate meta --from 17as 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 printed406 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/skillspackage. A 17.x project has none: its skills were installed withskills addfrom the repository. Remove those, add the package at your@objectstack/specversion 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 -ynpx skills listshows what is installed first — the walk listed the tenobjectstack-*skills — and the removal deletes only the names it is given. Run before the removal, the sync skips every skill askills addinstalled, exits0, 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 projectnpm install --save-devexited1withnpm error Cannot read properties of null (reading 'edgesOut'), so add the package with the manager that installed the project (create-objectstackuses 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, thenos 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-overlayslists the environment's stored permission-set and position rows whose name a package now holds, which a v18 cold boot refuses with422 NAMESPACE_CONFLICT; the preview exits1when it lists a row, and--applydeletes them (db9cf80,e030d43). On the walk's 17.7.0-created database it exited0:✓ 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 thecom.objectstack.organization-ownership-boot-guardplugin (error codeORGANIZATION_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-destructivefirst. Before the ceremony, the boot log andos migrate planlist theorganization_idcolumns as orphaned and nameos 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 exited1and 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
--applyasks you to state that you took one. For SQLite, stop the server and copy the database file with its-waland-shmfiles, or usesqlite3 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 receiveNOT NULL. The walk's plan: 81 tables, 34 to receiveNOT 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 14organization_idcolumns, setNOT NULLon 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_ownerandadmin_full_access; the three notes read back with their organization, and a fourth was written; an API key minted on 17.7.0 still answered200. 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 thegroupwalk's database also the indexidx_sys_sso_provider_organization_id). Carry the stored emoji reactions intosys_comment_reactionrecords first —os migrate comment-reactionsto preview, thenos migrate comment-reactions --apply --yes, which printed✓ Converted 1 reaction(s); … the run is recorded as verified(9963322,6673c80); without--yesit applies nothing and asks for it. Then runos migrate planand read its destructive entries: ⛔ go on only if they are the ceremony's leftovers listed here.os migrate apply --allow-destructiveapplies 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 runos migrate apply --allow-destructivein a terminal: it prints the changes and asksApply 4 change(s) to DATABASE? [y/N](four on thegroupwalk's copy); answeryonly when the list is the one you checked (nprintedℹ 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 exits0: read the output, not the exit code. The walk itself ranos migrate apply --allow-destructive --yesafter that check, which dropped the three columns; the next boot logged no drift, and the reaction read back fromsys_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
fieldsofcreate_record/update_record,assignmentvalues, each operand of a CRUD node'sfilter, anotifynode'srecipients,sourceIdandtemplateDatavalues, aloop(with abody) ormapcollection, asubflow/mapinput, ascriptnode'sinputs, ascreennode'sdefaults,defaultValueandrecordId, and anhttpnode'sbody.'{record.owner}'→{ dialect: 'cel', source: 'record.owner' };'{$User.Id}'→current_user.id(guard it withcurrent_user != nullin 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 butsourceId,templateData,defaultsandrecordId. - Text slots take
{{ }}holes. Anotifynode'stitle,messageandactionUrl, ascreennode'stitleanddescription, a refusingendnode'smessage, anhttpnode'surlandheadersvalues, and the translated screen headingflows.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) onnotifytitle,messageandactionUrl,screentitleandhttpurl. - CEL reads only the roots the flow binds.
user.id,ctx.user.id,os.user.id→current_user.id.recordis bound only on a flow an entrance hands a record (a record trigger, atype: 'flow'action, an inbound hook, …): elsewhere read the variable by its name or asvars.NAME. A condition is CEL:'{amount} > 100'→'amount > 100'— the old spelling is refused asinvalid CEL predicate(5fb1746,0f77ff5,243dd3c,7958bdf). Probed on 18.0.0 (os validate). - No
$names of your own. AnoutputVariable, anerrorVariableother than the default$error, aniteratorVariable/indexVariable, anidVariable, a screen field'snameand a declared variable'snamedrop the$, and every read of them follows (3e72f93,9f5eca5). Probed on 18.0.0 (defineStack) foroutputVariable,errorVariableanditeratorVariable. - 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'svisibleIf→visibleWhen; a loop'sitemVariable→iteratorVariable; delete anhttpnode'soutputVariable; asubflowconfig.timeoutMsmoves 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→objectNameandconfig.filters→filter; anotifynode'sto/subject/body/url→recipients/title/message/actionUrl, andsource: { object, id }→sourceObject/sourceId;map/subflowflow→flowName;scriptfunctionName/input→function/inputs; node typeshttp_request,http_callandwebhook→http; awaitnode's looseconfigkeys →waitEventConfig(duration→timerDuration,signal→signalName,timeoutMs→timerDurationas a string) and aconnector_actionnode's →connectorConfig.os migrate meta --from 16 --writewrites the ones it can prove (5fd2c76,f901746,d176b0a). Probed on 18.0.0 (defineStack), each but the nestedsourcepair. - An edge names two nodes of its graph, once. An edge whose
sourceortargetis 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
retryPolicywith nomaxRetries, or0, is refused: writemaxRetries: 3(what such a block ran with before 17), or delete the block for a single attempt;backoffMultiplierdefaults to1, so state2to 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
ComponentPropsMaprow does not declare, a value the row refuses, a retired prop, or a missing data binding failsos validate,os buildandos lint, and the metadata save door refuses the page (4f68ba1,32fa194,d0c09e4). Thecomponent-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 flatobject/filter/sort/limitofelement:record_picker,element:numberandelement:repeatermove to the node-leveldataSource, a sibling oftype;object-griddefaultFilters→filterwherefilteris 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'svisibilityand a form field'svisibleOn→visibleWhen(105902f);page:headerdescription→subtitle(8693110); deletepage:headershowStar(ae0ca66); delete amarketplace:installed-listcomponent (14a8bd2). Probed on 18.0.0. - Page layout refusals. A
mainregion takes onlywidth: 'full'— deletewidthto keep the page as it rendered (3a734e0); a record page that authors bothslots.detailsandslots.tabsis refused — moverecord:detailsinto the first tab'schildren(e223152); at most oneisDefault: truerecord page per object (5f37d36). Probed on 18.0.0 (defineStack). - List views. A
type: 'chart'view bindschart: { dataset, values }(20e7d52); deletefilterableFields—userFilters.fieldsdeclares 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'sdataSource(1912af4,05ab9ef). Probed on 18.0.0 (defineStack). - List views are editable in place by default. A
userActionsblock that leaveseditInlineout now parses toeditInline: true: writeuserActions: { 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'starget, a page action row's url target, a dashboard header action'sactionUrlandonSuccess.navigatetakehttp://,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'sunique: true→unique: 'global'(the same index, nothing on disk changes) orunique: 'organization'(ef1fcb2). Probed on 18.0.0 (defineStack). - A
controlled_by_parentobject has exactly one requiredmaster_detail. Anything else is refused, as an object or as anobjectExtensionsentry; if the rows must outlive their parent, give the object its ownsharingModel(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
stageFieldcarrieskind: '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
visibleWhenreads what the server binds:record,previousand the acting user (current_user, and its aliasesuser,ctx,os).parent,ctx.locale,os.envandcurrent_user.emailare 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_metadataand 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
functionswith onename, are refused, naming both (1959aac,147a0d1).defineSeed()refuses a record key that is no column of the object (9ad3d24). Probed on 18.0.0 forobjects,functionsanddefineSeed.
Positions, permission sets and sharing rules
- A position names its permission sets in
permissionSetson its definition. A boot-time binder that wrotesys_position_permission_setrows has nothing left to write to: the four catalog row objects are retired (bfc15d2,66949b7). Probed on 18.0.0 (defineStackacceptspermissionSets); see the data line below for the rows. - Permission-set keys. Delete
managedBy(bbdfc05) andadminScope.manageBindings/adminScope.authorEnvironmentSets(be48c5e); a bodypackageIdthat 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_ruleis 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
permissionsis 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', andOS_DATABASE_DRIVER=mongono longer selects a driver:os serveexits1with✗ Unsupported driver "mongo" (OS_DATABASE_DRIVER / --database-driver).(bc0538a). Probed on 18.0.0 (defineStack, andos servein the walk's app).- Email templates. Delete
categoryandisSystem(ec7f7f8). Templates resolve from the registry: declare them in code — an organization can no longer create or edit asys_email_templaterow, and the built-in seed is gone (7de0265,8c5aa50).categoryprobed 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_webhookrow is written for one, asys_webhookrow 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
apihook verifies only the timestamped signature. Sendx-objectstack-signature: t=UNIX_SECONDS,v1=HEX, the HMAC-SHA256 ofUNIX_SECONDS.RAW_BODYunder the flow's secret, within 300 seconds of the server clock; the body-onlysha256=…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 buildrefuses some hook bodies it used to lower: one that reachesctx.dispatch.scopeorctx.submitted— do the work atctx.dispatch.index === 0and readctx.previous— and a parameter list it cannot rebind to the sandbox'sctx(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
TS2305after the upgrade: delete the import.normalizeVisibleWhen,VISIBILITY_ALIAS_KEYSandVISIBILITY_STRICT_OPTIONS(readvisibleWhen;2c702fd,ef1fcb2);CONVERSION_CONFLICT_CODEand 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 onGET /meta/types(e23eccb). Read off the 18.0.0 tarball (@objectstack/spec). - Hand-built kernels.
I18nServicePluginlosesregisterRoutes/basePath— the runtime dispatcher owns/api/v1/i18n(249130f);SecurityPluginrefuses a kernel that registers nosys_user/sys_member(c6fe02d). Read off the 18.0.0 tarball (@objectstack/service-i18n,@objectstack/plugin-security). os meta resyncis deleted (9e9e6f9): it answersError: command meta:resync not found, exit2. Measured in the walk.- Tests on
@objectstack/verify.bootStackcomposes whatobjectstack servecomposes from the same configuration, and boots the productionsingleshape (7098aca,97610a5,34dba5a);runDashboardWindowProofstakes 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_atgets 90 days, and the response echoes the date; one asked for beyond a year is refused; a key unused for 90 days answers403 API_KEY_INACTIVE; a revoked or expired key answers403 API_KEY_REVOKED/API_KEY_EXPIREDwhere it used to answer401. 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 theauthsettingsapi_key_default_expiry_days,api_key_max_expiry_daysandapi_key_inactivity_days(1d79556). In the walk the 17.7.0 key still answered200, withexpires_atstamped one year after the first 18.0.0 boot; a new key gotexpires_at90 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
403codes 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.comcame back with noaccess-control-allow-origin. SetOS_CORS_ORIGIN=https://app.example.com(comma-separated for several), or the server plugin'scors.origins. A list holding'*'while credentials are on fails the boot: name the origins, or setOS_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.jsonandGET /api/v1/i18n/localesanswered401 UNAUTHENTICATEDwith no session (166a94f,b53b949). Measured in the walk. -
Request bodies are read as sent. A JSON body sent as
text/plainanswered400 INVALID_REQUEST(The request body was sent as text/plain, which this server does not read into a request body.); anullor non-record body on a/datacreate answered400 VALIDATION_FAILED, as did aPATCHwhoseexpectedVersionwas the number3— 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_setandsys_capabilityanswered404; read positions and permission sets through/api/v1/meta/positionand/api/v1/meta/permission. A grant row insys_user_permission_setnames its set inpermission_set(the set's name), and carries nopermission_set_id(66949b7,6f21216,0253192). Measured in the walk. -
Upload scopes are a closed list:
user,tenant,private,temp,attachments.scope: 'public'answered400 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 carriesacl: 'public_read'. The storage settings'max_upload_mbis enforced (413 PAYLOAD_TOO_LARGE) (6a3fe25,ee8751d,59d993c). Scope measured in the walk;PAYLOAD_TOO_LARGEread off the 18.0.0 tarball (@objectstack/service-storage). -
Emoji reactions are records.
sys_comment.reactionsis retired: read and writesys_comment_reaction(9963322). Measured in the walk. -
A public form's submit answers
{ id }only, not the stored record (7806a14). An import'srunAutomations: falseandtreatAsHistorical: trueare honoured only for the system principal and refused403 PERMISSION_DENIEDto any other caller (741aa85). A search over an object with no searchable field is refused400 INVALID_FIELDinstead 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 serveboot 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 serveexited1before 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_SECRETto a long random value, identical across restarts and nodes (the walk set the olderAUTH_SECRET, which 18.0.0 still reads);os devuses a development fallback.os startis not refused: with no secret set it wroteHOME_DIR/auth-secret(mode0600) on its first run and served sign-up. Measured on 18.0.0. -
A declared sharing rule served under
singlecarriesdeploymentWide: truewhere it carriedorganization_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_accessgrant is the only platform-admin anchor; os migrate applyon an empty database writes the organization-ownership birth marker (#24875);POST /api/v1/packages/:id/adopt-orphansrefuses a read-only package with422 WRITABLE_PACKAGE_REQUIRED(#24861; absent from the 18.0.0@objectstack/runtime);- on PostgreSQL,
introspectSchemareads each table in its own schema (#24920);os lintreports a retiredfilterableFieldsby 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.
v18
The v18 line and its GA release, 18.0.0, which closes the `18.0.0-next.N` prerelease train and publishes to npm's `latest` tag. It carries ADR-0131 (organization ownership is total, with no NULL `organization_id`) and landed its breaking changes in stages, each with its own migration.
v17
The v17 line — a truth-telling release. Files become owned records, the export privilege stops riding on read, the SDK is reconciled against the routes the server mounts, and a boot that cannot reach its datasource stops pretending it can. Per-release notes for 17.0.0 through 17.7.0.