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
40 changes: 40 additions & 0 deletions .changeset/spec-routes-doc-redirect-http-server.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
---
"@objectstack/spec": patch
---

docs(spec): stop recommending `contributes.routes` for code-handler endpoints — redirect the author-facing materials to the imperative `http.server` mount (#10726)

`contributes.routes` (`packages/spec/src/kernel/manifest.zod.ts`) has **zero readers**
monorepo-wide: the only non-test read of `manifest.contributes` anywhere reads `kinds`, not
`routes` (`packages/objectql/src/engine.ts:4499`). An author following the shipped guidance
writes a `contributes.routes` entry, gets a clean parse, and serves nothing — ADR-0049's
silent no-op with a published recommendation attached.

Per the maintainer ruling (2026-08-22, Option B), the four author-facing materials that
recommended the key are corrected **now**, ahead of and independent of the key's removal:

- `skills/objectstack-api/SKILL.md` — the `apis:` decision table's second row now names the
imperative `http.server` mount and states explicitly that `contributes.routes` parses and
serves nothing.
- `packages/spec/src/api/dispatcher.zod.ts` — the HttpDispatcher protocol doc no longer
claims it "supports dynamic route registration from plugins via contributes.routes".
- `docs/adr/0088-metadata-kind-admission-and-retirement.md` — the `router` retirement row no
longer credits `contributes.routes` as a delivered form. The `router` KIND's retirement is
unaffected: its delivered forms are the imperative mount and, since #5040, declarative
`apis:`.
- `packages/spec/src/ui/app.zod.ts` — the `App.apis` removal message no longer sends
migrators to `contributes.routes`.

The replacement recommendation was verified live on `main` before it was written, so this is
not a redirect to a second dead form: `http.server` is registered by
`packages/plugins/plugin-hono-server/src/hono-plugin.ts:271` (`providesServices` at :228) and
mounted by real in-tree consumers — `examples/app-showcase/src/system/server/recalc-endpoint.ts`
resolves it on `kernel:ready` and mounts `POST /api/v1/showcase/recalc`, and
`plugin-approvals`, `plugin-sharing`, `cloud-connection` and the CLI's `serve` all resolve the
same service.

Schema-only change to prose: no key is added, removed or re-typed here. The
`contributes.routes` tombstone itself is #10724's, which is blocked on the `cloud` census
(#10812) — this changeset carries only the doc corrections the ruling ordered not to wait.
The two `content/docs/references/**` pages are the regenerated projection of the two `.zod.ts`
edits (`pnpm --filter @objectstack/spec gen:docs`), not hand edits.
6 changes: 5 additions & 1 deletion content/docs/references/api/dispatcher.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,11 @@ The dispatcher is the central routing component that:
1. Matches incoming request URLs against registered route prefixes
2. Delegates to the corresponding CoreService implementation
3. Returns 503 Service Unavailable when a service is not registered
4. Supports dynamic route registration from plugins via contributes.routes
4. Serves prefixes registered by the kernel services above. Plugins that need
a code handler mount it imperatively on the `http.server` service (resolve
it from the plugin context on `kernel:ready`), NOT through the manifest's
`contributes.routes` key — nothing reads that key, so an entry there parses
cleanly and serves nothing.

Architecture alignment:
- Kubernetes: API server aggregation layer
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/ui/app.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,7 +80,7 @@ const result = ActionNavItemSchema.parse(data);
| **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
| **requiredPermissions** | `string[]` | optional | Permissions required to access this app |
| **objects** | `never` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps/<manifest.namespace>/<subpath>` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE still belongs in a plugin manifest `contributes.routes` entry. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps/<manifest.namespace>/<subpath>` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE is mounted imperatively instead: resolve the `http.server` service from your plugin context and register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — nothing reads it, so an entry there parses cleanly and serves nothing). Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
| **sharing** | `never` | optional | [REMOVED] `App.sharing` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049 enforce-or-remove) — no public-app route ever read it, so it declared sharing that did not exist. Public access is granted per FORM VIEW (`FormView.sharing`, the public-data-collection surface). Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
| **embed** | `never` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
| **mobileNavigation** | `never` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. |
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0088-metadata-kind-admission-and-retirement.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,7 +37,7 @@ Their real, consumed forms already live elsewhere, and all are **code contributi

| Retired kind | Delivered form |
|---|---|
| `router` | plugin manifest `contributes.routes` (HttpDispatcher prefix routing) + imperative `http.server` mounts. ⚠️ **Correction (#4936, 2026-08-04):** this row originally also credited app-authored declarative `apis:` as "executed by `handleApiEndpoint`". That was never true — no route was mounted for a declared path and `matchEndpoint` had no implementation anywhere, so the branch was dead code; it has been deleted and a non-empty `apis:` is now rejected at publish/validate. The retirement decision for the `router` KIND is unaffected (its delivered forms are the two above, both code contributions); the endpoint executor is being built under #5040, after which declarative `apis:` becomes a third, real delivered form. |
| `router` | imperative `http.server` mounts (resolve the service from the plugin context and mount on `kernel:ready`) + declarative `apis:` since #5040. ⚠️ **Correction (#4936, 2026-08-04):** this row originally also credited app-authored declarative `apis:` as "executed by `handleApiEndpoint`". That was never true — no route was mounted for a declared path and `matchEndpoint` had no implementation anywhere, so the branch was dead code; it has been deleted and a non-empty `apis:` is now rejected at publish/validate. The retirement decision for the `router` KIND is unaffected; the endpoint executor is being built under #5040, after which declarative `apis:` becomes a real delivered form. ⚠️ **Correction (2026-08-22 ruling):** this row also credited plugin manifest `contributes.routes` (HttpDispatcher prefix routing) as a delivered form. That was never true either — the key has zero readers monorepo-wide, so an entry parses cleanly and serves nothing. The key is being removed and every author-facing material that recommended it is corrected to point at the imperative mount; the delivered forms are the two now named above, and the `router` KIND's retirement is unaffected. |
| `function` | `defineStack({ functions })` code values (hook-binder & flow `script` body runners) + plugin `contributes.functions` (QL query functions) |
| `service` | the plugin/service registry itself (`registerService`) |

Expand Down
6 changes: 5 additions & 1 deletion packages/spec/src/api/dispatcher.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,11 @@ import { CoreServiceName, ServiceCriticalitySchema } from '../system/core-servic
* 1. Matches incoming request URLs against registered route prefixes
* 2. Delegates to the corresponding CoreService implementation
* 3. Returns 503 Service Unavailable when a service is not registered
* 4. Supports dynamic route registration from plugins via contributes.routes
* 4. Serves prefixes registered by the kernel services above. Plugins that need
* a code handler mount it imperatively on the `http.server` service (resolve
* it from the plugin context on `kernel:ready`), NOT through the manifest's
* `contributes.routes` key — nothing reads that key, so an entry there parses
* cleanly and serves nothing.
*
* Architecture alignment:
* - Kubernetes: API server aggregation layer
Expand Down
6 changes: 4 additions & 2 deletions packages/spec/src/ui/app.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1458,8 +1458,10 @@ export const AppSchema = lazySchema(() => strictObject(
'only thing that opens anonymous access, and ADR-0121 D6 then requires an armed ' +
'`rateLimit: { enabled: true, windowMs, maxRequests }`. Read the ' +
'`declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a ' +
'security review, not a rename. A route that genuinely needs handler CODE still ' +
'belongs in a plugin manifest `contributes.routes` entry. ' +
'security review, not a rename. A route that genuinely needs handler CODE is mounted ' +
'imperatively instead: resolve the `http.server` service from your plugin context and ' +
'register the route on `kernel:ready` (NOT the manifest `contributes.routes` key — ' +
'nothing reads it, so an entry there parses cleanly and serves nothing). ' +
'Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand.',
),

Expand Down
4 changes: 2 additions & 2 deletions skills/objectstack-api/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -156,12 +156,12 @@ behind `/api/v1/data/{object}`, `flow` to the automation pipeline behind
`POST /api/v1/automation/{name}/trigger`. An endpoint is a stable URL plus a
policy layer over an existing pipeline, never a second execution dialect.

### Choosing between `apis:` and `contributes.routes`
### Choosing between `apis:` and a code handler

| Use | When |
|:---|:---|
| **`defineStack({ apis })`** | The endpoint is a *projection* of something the platform already executes: query/return records, or trigger a flow. No code, no deploy artifact, publish-gated. **Prefer this.** |
| **`contributes.routes`** (plugin manifest) | The endpoint needs real handler CODE — a third-party callback with its own signature verification, a streaming response, a protocol the platform does not speak. This registers a prefix for a plugin-provided service. |
| **`http.server` mount** (plugin code) | The endpoint needs real handler CODE — a third-party callback with its own signature verification, a streaming response, a protocol the platform does not speak. Mount it on `http.server`; `contributes.routes` parses but serves nothing. |

If the logic is "a bit of computation, then a record write", express it as a
**flow** and point a `type: 'flow'` endpoint at it — that keeps the URL
Expand Down
Loading