You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: content/docs/getting-started/quick-reference.mdx
+32Lines changed: 32 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -284,6 +284,38 @@ import type { Field, ServiceObject, QueryAST } from '@objectstack/spec/data';
284
284
importtype { View } from'@objectstack/spec/ui';
285
285
```
286
286
287
+
### Declarative Endpoints (`apis:`)
288
+
289
+
Declared on the **stack** (`defineStack({ apis })`), live from protocol 17 — each entry
290
+
is gated at publish and, once it passes, serves real traffic. Full contract:
291
+
[HTTP API → Declarative Endpoints](/docs/protocol/kernel/http-protocol).
292
+
293
+
| Rule | Value |
294
+
|:---|:---|
295
+
|**Path shape**|`/api/v1/apps/<manifest.namespace>/<subpath>` — only the subpath is yours (ADR-0121 D1) |
296
+
|**Namespace**| must be declared **explicitly** as `manifest.namespace`; never derived from `manifest.id` (D2) |
297
+
|**Types that execute**|`object_operation` (needs `objectParams.object` + `.operation`) and `flow` (needs `target`). `script` / `proxy` are rejected at publish |
298
+
|**`authRequired`**| defaults to `true` — **omitting it is safe**. An explicit `false` is the only thing that opens anonymous access |
299
+
|**`authRequired: false`**| REQUIRES an armed budget, `rateLimit: { enabled: true, windowMs, maxRequests }` (ADR-0121 D6) — `enabled` itself defaults to `false`, so a budget without it meters nothing |
300
+
|**`cacheTtl`**| seconds, GET-only, applied to successful answers only (`Cache-Control: private, max-age=<ttl>`) |
// Defaults to `true`. Omitting it is safe; see the policy table below.
1091
+
authRequired: true,
1092
+
// Seconds. GET-only, and only ever on a successful answer.
1093
+
cacheTtl: 30,
1094
+
},
1095
+
],
1096
+
});
1097
+
```
1098
+
1099
+
### How a request is served
1100
+
1101
+
Declared endpoints are **not registered routes**. They run in the dispatcher's
1102
+
unmatched-request seam, which is what makes it structurally impossible for a
1103
+
declaration to shadow a built-in route:
1104
+
1105
+
1.**Match** — the request path must be under `<prefix>/apps/`, and `METHOD` + path
1106
+
(one trailing slash trimmed) must hit exactly one declaration.
1107
+
2.**Policy chain** — `rateLimit` → `authRequired` → `cacheTtl`, in that order. Metering
1108
+
runs *before* the auth gate on purpose: the traffic that most needs a budget
1109
+
(credential stuffing, scraping) is exactly the traffic that ends in a 401, so a denied
1110
+
request still spends a token.
1111
+
3.**Delegation** — a request that passed is executed by the **same pipelines the
1112
+
built-in routes use**, under the caller's own execution context, so RLS/FLS and the
1113
+
exposure gate apply identically. A declared endpoint is a stable URL plus a policy
1114
+
layer over an existing pipeline, never a second execution dialect.
1115
+
1116
+
| Endpoint declares | Answer |
1117
+
|:---|:---|
1118
+
|`type: 'object_operation'`| delegated to the same `callData` binding that serves `/api/v1/data/{object}` — byte-identical `data`|
1119
+
|`type: 'flow'`| delegated to the same automation pipeline as `POST /api/v1/automation/{name}/trigger`|
1120
+
|`authRequired: true` (or omitted) + anonymous caller |`401``UNAUTHENTICATED`, the same envelope every seam answers |
1121
+
|`rateLimit` armed and exhausted |`429` + `Retry-After`, never with a cache directive |
1122
+
|`cacheTtl: 30` on a successful GET |`Cache-Control: private, max-age=30` — `private` is a security rule, not tuning: any response can be RLS-trimmed |
1123
+
|`cacheTtl: 0`|`Cache-Control: no-store`|
1124
+
| an error answer (401/429/5xx) | never carries `Cache-Control`, and `outputMapping` is never applied to it |
1125
+
1126
+
### What an unmatched request answers
1127
+
1128
+
The endpoint seam **writes nothing** when it does not match, so it changes no existing
1129
+
answer. Both of these are the transport's own bare 404, byte for byte:
1130
+
1131
+
```http
1132
+
GET /api/v1/apps/acme/no-such-endpoint → 404 {"error":"Not found"}
1133
+
GET /api/v1/no-such-route → 404 {"error":"Not found"}
1134
+
```
1135
+
1136
+
A **method mismatch on a declared path is also a 404**, not `405` + `Allow`:
1137
+
1138
+
```http
1139
+
POST /api/v1/apps/acme/leads → 404 {"error":"Not found"}
1140
+
```
1141
+
1142
+
That is a consequence of the seam, not an inconsistency — nothing registered a route for
1143
+
that path, so there is no method set to report. A registered route still answers `405`
1144
+
when it exists and the verb does not fit — that contract is unchanged.
1145
+
1146
+
### The five publish gates
1147
+
1148
+
A declaration this runtime cannot serve is **rejected at publish**, naming the endpoint,
1149
+
the key and the fix — never parsed into silence. `objectstack validate` (or `os build`)
1150
+
runs the same gates your publish path does:
1151
+
1152
+
| Gate | Rejects |
1153
+
|:---|:---|
1154
+
|**Namespace** (ADR-0121 D1/D2) | a `path` outside `/api/v1/apps/<manifest.namespace>/<subpath>`, or a stack declaring `apis:` with no explicit `manifest.namespace`|
1155
+
|**Supported target**|`type: 'script'` / `'proxy'` (neither executes in 17.x), an `object_operation` missing `objectParams.object` or `.operation`, a `flow` naming no `target`|
1156
+
|**Mapping**| a mapping `transform` (there is no transformation registry), an unusable dot path (empty segment, `__proto__`), two entries writing the same target path, or `inputMapping` on a `find` / `get` / `delete` operation that never reads a body |
1157
+
|**Policy**|`authRequired: false` without `rateLimit.enabled: true` (ADR-0121 D6), an unusable armed budget, a negative `cacheTtl`, or `cacheTtl` on a non-GET method |
1158
+
|**Uniqueness**| two endpoints in one stack claiming the same `METHOD` + path |
1159
+
1160
+
<Callouttype="warn">
1161
+
**`authRequired: false` is the one answer that cannot be taken back.** It defaults to
1162
+
`true`, so omitting it is safe; an explicit `false` is the only thing that opens an
1163
+
unauthenticated execution entry point, and ADR-0121 D6 pairs it with an **armed** budget
1164
+
— `rateLimit.enabled` itself defaults to `false`, so writing only `windowMs` /
1165
+
`maxRequests` declares a budget that meters nothing. The gate checks
1166
+
`enabled === true`, not the key's presence.
1167
+
</Callout>
1168
+
1169
+
`inputMapping` / `outputMapping`**move and rename fields by dot path, and nothing more**
1170
+
— `inputMapping` projects the request body before delegation (so it can never buy a
1171
+
caller past the policy chain), `outputMapping` projects a successful response body only.
Copy file name to clipboardExpand all lines: content/docs/references/ui/app.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -86,7 +86,7 @@ const result = ActionNavItemSchema.parse(data);
86
86
|**homePageId**|`any`| 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 rewrite existing sources automatically. |
87
87
|**requiredPermissions**|`string[]`| optional | Permissions required to access this app |
88
88
|**objects**|`any`| 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. |
89
-
|**apis**|`any`| optional |[REMOVED]`App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key. Note the stack-level `defineStack({ apis })` this prescription used to redirect to is ALSO not executable in v17 (#4936): the vocabulary is kept but a non-empty array is rejected there too, until the endpoint executor ships (tracked by https://github.com/objectstack-ai/objectstack/issues/5040). Serve the route in code meanwhile — a plugin manifest `contributes.routes` entry or an `http.server` route. |
89
+
| **apis** | `any` | 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. |
90
90
|**sharing**|`any`| 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. |
91
91
|**embed**|`any`| 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. |
92
92
|**mobileNavigation**|`any`| 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. |
0 commit comments