Skip to content
Merged
21 changes: 21 additions & 0 deletions .changeset/react-pages-default-off.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@objectstack/cli': patch
'@objectstack/spec': patch
---

feat: `kind:'react'` pages are off unless the deployment turns them on

Clause-②: no
Metadata-protocol: none

**BREAKING**: the `kind:'react'` page tier, which runs page source in the Console, now defaults to **off** (ADR-0081, amended). A deployment that serves react pages turns the tier on with `OS_PAGE_REACT_ENABLED=true`, in the environment or the project's `.env` file. With it off, each react page shows a notice naming that switch instead of running, and the Console document's content-security-policy no longer allows `'unsafe-eval'`. `kind:'html'` pages are unaffected.

To keep react pages running after the upgrade, set `OS_PAGE_REACT_ENABLED=true` on the deployment. Stored pages need no change.

<!-- adr-0087: not-required (no-migration-prescription) an operator environment default moved; no spec key, export or stored row changed, so there is nothing for objectstack migrate meta to rewrite -->

The earlier name is still read while the new one is unset: `OS_PAGE_REACT=on` turns the tier on and `OS_PAGE_REACT=off` keeps it off, each with a one-time deprecation warning. `true`, `1`, `on`, `yes` and `enabled` turn it on; unset, `false`, `0`, `off`, `no` and `disabled` leave it off; any other value leaves it off and is reported once in the server log.

The server now writes the Console's capability global in both directions, so a Console build with either default follows the server's setting. `examples/app-showcase` opts in through its `.env`, so its three react pages keep running. The `PageSchema.kind` description in `@objectstack/spec` now states the new default.

Operators who collect content-security-policy reports may see one `script-src` eval refusal per Console load while the tier is off: the Console's schema library probes for `eval` once at boot and falls back without it. Nothing on the page is affected, and the probe is removed on the objectui side.
6 changes: 3 additions & 3 deletions content/docs/protocol/objectui/layout-dsl.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ write the page body as a string. Pick by what the page needs:
For `html` / `react`, the page body is the `source` string (it is the
source-of-truth; `regions` is ignored). `html` is **safe by construction** (it
is parsed into the same SDUI tree this document describes, never executed).
`react` executes author code, so it is gated by a host capability that is **ON
by default** and disabled per-deployment with the `OS_PAGE_REACT=off` env
variable. Both tiers can embed the platform's real data components
`react` executes author code, so it is gated by a host capability that is **OFF
by default** and turned on per deployment with the `OS_PAGE_REACT_ENABLED=true`
env variable. Both tiers can embed the platform's real data components
(`<ObjectTable>`, `<ObjectForm>`, charts, metrics).

> **Learn more:** [ADR-0080](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0080-ai-authored-ui-jsx-source.md)
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/ui/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ View filter rule
| **interfaceConfig** | `{ source?: string; columns?: string[] \| object[]; sort?: object[]; filterBy?: object[]; … }` | optional | Interface-level page configuration (for Airtable-style interface pages) |
| **aria** | `{ ariaLabel?: string \| Record<string, string>; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes |
| **print** | `{ paperSize?: Enum<'A4' \| 'A5' \| 'Letter' \| 'Legal'>; orientation?: Enum<'portrait' \| 'landscape'>; margins?: object; repeatHeader?: boolean; … }` | optional | Print declaration — makes this page a print page, a document authored in the page's own blocks: paper size and orientation, margins in millimetres, the running header and footer (the page's own `header` / `footer` regions), page numbers and page-break hints, each mapped to print CSS. A print page is a `kind: 'full'` page of `type: 'record'`, `'home'` or `'app'` with its blocks in `regions`, and every block in it must come from the printable block subset: containers that draw every child (`page:section`, `page:card`, `page:footer`), field blocks (`record:details`, `record:highlights`), the child-record table `record:line_items`, `element:text`, `element:image`, `element:divider`, `element:definition-list`, `element:repeater`, `element:number` and `object-metric`; `os validate` / `os build` / `os lint` and the metadata save door refuse any other block inside it. Applied by the console's browser print rendering, which prints the page through the browser's own print dialog (where it can be saved as PDF); the server-side PDF renderer is to follow. A list view's print button is `allowPrinting`, not this. |
| **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; ADR-0081 named it html), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline `style` with the same token colors; it runs author JS, so it is gated by a host capability that defaults ON and is disabled server-side via the OS_PAGE_REACT=off env toggle. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). |
| **kind** | `Enum<'full' \| 'slotted' \| 'html' \| 'react'>` | optional (default: `"full"`) | Page override mode. full \| slotted = structured authoring; html = author-written constrained JSX compiled (parsed, never executed) to the tree (ADR-0080; ADR-0081 named it html), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors; react = real-React source executed at render by the runtime (ADR-0081), styled by inline `style` with the same token colors; it runs author JS, so it is gated by a host capability that defaults OFF and is turned on server-side via the OS_PAGE_REACT_ENABLED=true env toggle. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). |
| **slots** | `{ header?: object \| object[]; actions?: object \| object[]; alerts?: object \| object[]; highlights?: object \| object[]; … }` | optional | Slot override map for slotted pages |
| **source** | `string` | optional | Page source text. For kind==='html' it is constrained JSX compiled to the tree by @objectstack/sdui-parser at save time (parse, never execute), styled by the registered components' structured props plus a JSON `style` object with hsl(var(--token)) theme colors. For kind==='react' it is real React/JSX executed at render by @object-ui/react-runtime (trusted tier), styled by inline `style` with the same token colors. Do not author Tailwind classes in page source in either tier: `source` is runtime metadata the build-time Tailwind never scans, so utility classNames silently produce no CSS (ADR-0065; ADR-0080). Authoritative over `regions` in both. |
| **requires** | `string[]` | optional | Plugin namespaces the page's source uses, derived from the source at save — omit it. The key exists only on a kind==='html' page, the kind whose source is compiled at save; on a 'react', 'full' or 'slotted' page — and a page that omits kind, which is 'full' — it is refused at parse. On a server that has the deployment's SDUI component manifest, saving an html page compiles its source and stores the namespaces it uses here; a written list that disagrees with the source is refused (422 INVALID_METADATA, page-requires-disagrees-with-source) — on a draft save it is kept until the draft's publish, which refuses it. At load, a stored page whose list names a plugin no component in that manifest carries is reported, page and plugin named, and is still served. A server with no manifest checks neither and says so once at boot. |
Expand Down
5 changes: 3 additions & 2 deletions content/docs/ui/pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ is rejected by the schema rather than rendering empty.
`'jsx'` keep loading as `html` pages.

Because `kind: 'react'` executes author JavaScript, it is gated by a host capability that
defaults **ON** and is disabled server-side per deployment with `OS_PAGE_REACT=off`. The
`html` tier is unaffected by that switch — it is never executed.
defaults **OFF** and is turned on server-side per deployment with
`OS_PAGE_REACT_ENABLED=true`. The `html` tier is unaffected by that switch — it is never
executed.

<Callout type="warn">
Neither source tier is styled with Tailwind utility classes. A page's `source` is
Expand Down
25 changes: 16 additions & 9 deletions content/docs/ui/react-pages.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,27 @@ The two tiers also differ in what they give you for layout, in opposite directio

## The security gate

Because the react tier runs author JavaScript, it is gated by a host capability named
`react-pages`, which defaults **ON** — the platform's assumption is that page authors
are reviewed and draft-gated.
Because the react tier runs author JavaScript in every viewer's session, with no sandbox,
it is gated by a host capability named `react-pages`, which defaults **OFF**. The tier is
for deployments whose page authors are trusted and reviewed.

A deployment that does not trust its page authors turns the tier off **server-side**:
A deployment that trusts its page authors turns the tier on **server-side**:

```bash
OS_PAGE_REACT=off objectstack start
OS_PAGE_REACT_ENABLED=true objectstack start
```

The server then injects the console's capability-disable flag, and every `kind:'react'`
page renders an explanatory notice instead of executing. `off`, `0`, `false`, `no` and
`disabled` are all accepted spellings. `kind:'html'` pages are unaffected — they were
never executed in the first place, which is the whole point of the split.
or with the same line in the project's `.env` file. `true`, `1`, `on`, `yes` and `enabled`
turn it on; unset, `false`, `0`, `off`, `no` and `disabled` leave it off. Any other value
leaves it off and is reported once in the server log. The earlier name `OS_PAGE_REACT`
(`on` / `off`) is still read when `OS_PAGE_REACT_ENABLED` is unset, with a deprecation
warning.

With the tier off, every `kind:'react'` page renders a notice that names the switch
instead of executing, and the Console's content-security-policy refuses `eval`. With it
on, the server tells the Console to run react pages and the policy allows the `Function`
call the runtime compiles them with. `kind:'html'` pages are unaffected either way — they
are never executed, which is the whole point of the split.

## What is in scope

Expand Down
4 changes: 3 additions & 1 deletion docs/adr/0081-trusted-react-page-tier.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0081: A trusted `kind:'react'` page tier — real React executed in the main tree, gated by a host capability; and renaming `kind:'jsx'` → `kind:'html'`

**Status**: Accepted (2026-06-30) — framework scope implemented (`html`/`react` page kinds + `jsx` alias, lint split `validate-jsx-pages`/`validate-react-pages`, `OS_PAGE_REACT=off` server toggle); the `@object-ui/react-runtime` executor + `CAP_REACT_PAGES` gate live in objectui.
**Status**: Accepted (2026-06-30) — framework scope implemented (`html`/`react` page kinds + `jsx` alias, lint split `validate-jsx-pages`/`validate-react-pages`, `OS_PAGE_REACT=off` server toggle); the `@object-ui/react-runtime` executor + `CAP_REACT_PAGES` gate live in objectui. **Amended** (2026-10-25 — the `react` tier defaults **OFF**; a deployment opts in with `OS_PAGE_REACT_ENABLED=true`. See "Amendment (2026-10-25): default off, opt in per deployment".)
**Deciders**: ObjectStack Protocol Architects
**Builds on**: [ADR-0080](./0080-ai-authored-ui-jsx-source.md) (AI authors a *constrained* JSX text, parsed-never-executed, into the SDUI tree; the component registry is the contract; capability ≠ contract), [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (AI writes metadata via draft-gated `saveMetaItem` — the human-review boundary), [ADR-0077](./0077-authoring-surface-boundary-hook-flow-validation.md) (route by intent; verifiability tier = friction tier; **code is the flagged escape hatch**), [ADR-0078](./0078-no-silently-inert-metadata.md) (no silently-inert metadata).
**Consumers**: `@objectstack/spec` (`packages/spec/src/ui/page.zod.ts` — `kind` gains `'html'` and `'react'`; `'jsx'` kept as a deprecated alias), `@objectstack/lint` (`validate-jsx-pages` — lints `html`/`jsx`, **skips** `react`), `../objectui` `@object-ui/react-runtime` (**new** — the trusted runtime-React executor) + `@object-ui/core` (the host capability gate) + `@object-ui/components` (PageRenderer routing + the full HTML tag set).
Expand All @@ -13,6 +13,8 @@

> **Amendment (2026-06-30 — ADR-0065 styling correction).** The "HTML + **Tailwind**" framing for page `source` is **superseded on styling**. A page's `source` is *runtime metadata*, so the console's build-time Tailwind never scans it — authored utility `className`s silently produce no CSS (the exact failure ADR-0065 was written to prevent; the Task Desk modal's `bg-black/50` backdrop rendered transparent). The tiers themselves stand; only the styling **primitive** changes: `kind:'html'` styles via the registered components' structured props + a JSON `style` object; `kind:'react'` styles via inline `style={{}}` with `hsl(var(--token))` theme colors and renders drawer/modal overlays through `<ObjectForm formType="drawer"|"modal">` (never a hand-rolled `fixed inset-0`). **Do not author Tailwind classes in page source.** See [ADR-0065](./0065-sdui-styling-model.md).

> **Amendment (2026-10-25): default off, opt in per deployment.** Decision 3 and TL;DR 4 are **superseded on the default**; the tier, its gate and its host-only control stand. `CAP_REACT_PAGES` now defaults **OFF** in `@object-ui/core`, and the ObjectStack console serving (`cli/utils/console.ts`, `readReactPagesTier`) turns it on only when the operator sets `OS_PAGE_REACT_ENABLED=true` (`OS_PAGE_REACT=on`, the earlier name, is still read through `readEnvWithDeprecation`). The server writes the capability global in **both** directions — the enable list when on, the disable list when off — so its answer holds against a Console build with either default. The Console document's content-security-policy keeps `'unsafe-eval'` tied to the same per-request read. Off, a react page renders the named "React pages are disabled on this deployment" notice. **Why the default moved:** "trust + review" names a property of one deployment's page authors, which only that deployment's operator can vouch for, so the platform default no longer vouches for it on every deployment; the operator makes the trust decision explicitly. Reference deployments that ship react pages opt in in their own configuration (`examples/app-showcase/.env`).

## TL;DR

1. **[rename] `kind:'jsx'` → `kind:'html'`.** The constrained, parse-never-execute tier (ADR-0080) is renamed to match what it is: author-written **HTML + Tailwind** (expressed as constrained JSX) compiled to the SDUI tree. `'jsx'` stays as a **deprecated alias** (already-saved pages keep loading); all authored examples/docs move to `'html'`.
Expand Down
Loading
Loading