Skip to content

feat: headless demo page that renders a table from a manifest URL - #123

Merged
mxkaske merged 9 commits into
mainfrom
feat/headless-demo
Nov 9, 2026
Merged

mxkaske merged 9 commits into
mainfrom
feat/headless-demo

Conversation

@mxkaske

@mxkaske mxkaske commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Summary

  • /headless (shortcut /h): a page with no schema of its own. It reads a manifest URL from ?manifest= and renders that endpoint. It starts on /drizzle/api/schema and has a second example, /headless/api/orders/schema, an in-memory array served as plain JSON with an inferred schema, offset paging and no chart or live mode. Inspect lists what the endpoint declares and runs runEndpointConformance against it in the browser. Any https:// endpoint with CORS headers can be pasted (http:// for localhost only); other URL forms are rejected.
  • data-table-remote: rendering DataTableRemote for the first time surfaced gaps.
    • searchParamsSerializer is optional and derived from the manifest. AGENTS.md and the Cursor rules already showed the component without it.
    • uuid and live are part of the URL state, so the detail sheet opens on a table with a select column and live mode has state to flip.
    • commandSlot and toolbarActions may be functions of the resolved manifest, filter schema, refresh and fetchPreviousPage. sheetSlot receives the same context as a second argument.
    • extraColumns appends columns after the manifest's; defaultColumnVisibility is applied over the manifest's defaults.
  • data-table-sheet: a record value renders as JSON and a missing value as a dash, instead of [object Object] and null.
  • data-table-query (first commit): runEndpointConformance failed this repo's own Drizzle endpoint on size, because createDrizzleHandler returns a group of rows sharing one cursor value whole (six here, probe size five). An oversized page now passes when every row carries the page's nextCursor in a timestamp column, with a note in the check's detail. The manifest has to name that timestamp column; a body that is null or not an object is now a failed shape check instead of a throw. An endpoint that ignores size still fails. Docs describe size as a target.
  • Web app: the shared logs schema declares level-indicator and status-code as its wire displays (the custom cells still render on /infinite and /drizzle), the loading skeleton no longer uses Math.random() (hydration mismatch), and the live button takes its timestamp column as a prop. Docs, README, sitemap and the home examples list include the new page.

Row actions: DataTableRemote does not draw them itself, so a headless table does not have to install the actions block. It gains extraColumns and defaultColumnVisibility, and the host places the actions block's column and bulk bar. The page does this for the logs example, as /drizzle does, and only for a manifest this site serves. The docs show the wiring.

Test plan

  • Local checks pass (format, lint, typecheck, tests)
  • CI passes
  • Manually verified in browser (if UI change)
  • Registry rebuilt (if packages/registry/src/ changed)

Browser checks were run in headless Chromium against a local dev server: filters, sort, infinite scroll, detail sheet and its URL state, command palette, live mode, Inspect on both examples, a cross-origin endpoint with CORS, six rejected or failing URLs, mobile width and dark mode. The fixed kit was also run against the production /drizzle/api at its default probe size: 9 passed.

Breaking changes

None. Behaviour changes to note:

  • In DataTableRemote, a column whose key is a reserved request parameter (uuid, live, sort, cursor, direction, size) keeps its column but loses its filter, with a console warning. uuid and live are now part of the URL state.
  • The sheet's fallback for record and missing values changed as described above.
  • A passing conformance check may now carry a detail.
  • /headless/api/orders and its manifest send access-control-allow-origin: * (public sample data).

Summary by cubic

Adds a /headless demo page that renders a table from any manifest URL and closes the gaps in DataTableRemote that real usage surfaced.

  • /headless reads the manifest from ?manifest=, starts on /drizzle/api/schema, and ships a second plain-JSON endpoint (/headless/api/orders) with an inferred schema and offset paging. Manifest URLs are validated (https only, http for localhost; tabs, line breaks and spaces are rejected), and the rows URL is derived from the manifest path alone so a query string on the manifest is not copied onto row requests. This site's own endpoint pasted as an absolute URL is treated as served here, so it keeps row actions and loses the "another host" note.
  • Inspect lists what the endpoint declares and runs runEndpointConformance in the browser; any https:// URL with CORS headers can be pasted, http:// only for localhost. The rows response is validated against the list contract instead of rendering an empty table.
  • The endpoint row is the top strip of the table's main column on the logs example, whose row actions render the way /drizzle does: a bulk bar on a selection and a hidden row-menu column. Actions appear only for manifests this site serves.
  • DataTableRemote now derives its search param serializer from the manifest, keeps uuid and live in URL state, lets commandSlot, toolbarActions, and sheetSlot be functions of the resolved manifest, and takes extraColumns and defaultColumnVisibility so the host can append the actions block. It still does not draw row actions itself.
  • The detail sheet renders a record as JSON, a missing value as a dash instead of [object Object] and null, and [unserializable] for values JSON cannot write.
  • runEndpointConformance accepts a page larger than size when every row carries the page's nextCursor in a timestamp column named by the manifest; size is documented as a target. A body that is not an object is a failed shape check, not a throw.
  • The loading skeleton no longer uses Math.random() (hydration mismatch), and the live button takes its timestamp column as a prop.

Behavior changes

  • uuid, live, sort, cursor, direction, size, plus any key the pagination strategy owns (offset or a custom cursor key), are reserved URL-state keys in DataTableRemote; a filter on a column with any of them is dropped with a warning, and row selection is seeded from uuid only when there is no select column. readPath reads own properties only, so a primary key of toString or constructor no longer looks present on every row.
  • The sheet's fallback for record and missing values changed as described above, and a passing conformance check may carry a detail note.
  • /headless/api/orders and its manifest send access-control-allow-origin: * (public sample data).

Written for commit 9606feb. Summary will update on new commits.

View guided diff Turn on auto-fix

runEndpointConformance failed any endpoint that returned more rows than
`size`, including this repo's own Drizzle endpoint: createDrizzleHandler
returns a group of rows sharing one cursor value whole, because the next
page is read with a strict `<` and splitting the group would drop the
rest of it. The demo data writes six rows per timestamp and the kit
probes with five.

A page larger than `size` now passes when every row carries the page's
`nextCursor` in a timestamp column, with a note in the check's detail.
An endpoint that ignores `size` returns rows spread along the cursor and
still fails, as does an oversized page with no cursor.

Docs and the contract comment now describe `size` as a target.
/headless has no schema of its own. It takes a manifest URL from
`?manifest=`, starts on /drizzle/api/schema, and offers a second endpoint,
/headless/api/orders, served as plain JSON with an inferred schema and
offset paging. Inspect shows what the endpoint declares and runs
runEndpointConformance against it in the browser. Any https endpoint
with CORS headers can be pasted; other URL forms are rejected.

Rendering DataTableRemote for the first time surfaced gaps in the block:

- searchParamsSerializer is optional and derived from the manifest
- `uuid` and `live` are part of the URL state, so the detail sheet opens
  on a table with a select column and live mode has state to flip
- commandSlot and toolbarActions may be functions of the resolved
  manifest, filter schema, refresh and fetchPreviousPage
- the sheet shows JSON for a record and a dash for a missing value
  instead of "[object Object]" and "null"

The shared logs schema declares `level-indicator` and `status-code` as
its wire displays, the loading skeleton no longer uses Math.random()
(hydration mismatch), and the live button takes its timestamp column as
a prop. The docs say row actions are not drawn by DataTableRemote yet.
@vercel

vercel Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
data-table-filters Ready Ready Preview Nov 6, 2026 3:06pm UTC

Request Review

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 32 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/registry/src/components/data-table/data-table-remote/data-table-remote.tsx">

<violation number="1" location="packages/registry/src/components/data-table/data-table-remote/data-table-remote.tsx:504">
P2: `defaultRowSelection` is only applied on mount, so URL-driven row changes leave the old row selected after closing or navigating. Synchronize row selection with `search.uuid` (or avoid seeding it in multi-select mode); the sheet already reads the current UUID directly.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread apps/web/src/app/headless/client.tsx Resolved
defaultColumnFilters={defaultColumnFilters}
defaultColumnSorting={sort ? [sort] : undefined}
// Restores the open row from a shared link.
defaultRowSelection={

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: defaultRowSelection is only applied on mount, so URL-driven row changes leave the old row selected after closing or navigating. Synchronize row selection with search.uuid (or avoid seeding it in multi-select mode); the sheet already reads the current UUID directly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/registry/src/components/data-table/data-table-remote/data-table-remote.tsx, line 504:

<comment>`defaultRowSelection` is only applied on mount, so URL-driven row changes leave the old row selected after closing or navigating. Synchronize row selection with `search.uuid` (or avoid seeding it in multi-select mode); the sheet already reads the current UUID directly.</comment>

<file context>
@@ -330,6 +500,10 @@ function RemoteTableQuery({
       defaultColumnFilters={defaultColumnFilters}
       defaultColumnSorting={sort ? [sort] : undefined}
+      // Restores the open row from a shared link.
+      defaultRowSelection={
+        typeof search.uuid === "string" ? { [search.uuid]: true } : undefined
+      }
</file context>

Comment thread apps/web/src/app/headless/client.tsx Resolved
Page:
- reject tabs, line breaks and spaces in a manifest URL. Browsers strip
  them before parsing, so `/<tab>/host` was fetched as `//host` while
  being treated as a path on this site
- derive the rows URL from the manifest path alone, so a manifest URL
  with a query string no longer sends row requests to `/schema?v=2?...`
- validate the rows response against the list contract and report a
  wrong shape instead of rendering an empty table
- only the request started last may set the rows error message
- revive string dates inside a SuperJSON envelope too
- a conformance run that throws says so

data-table-remote:
- drop the filter of a column whose key is a reserved request parameter
  (`uuid`, `live`, `sort`, `cursor`, `direction`, `size`) and warn
- seed the row selection from `uuid` only without a select column; with
  one it ticked the row's checkbox

Conformance kit:
- a body that is `null`, a string or an array is reported as a failed
  shape check instead of throwing
- the tied-group exemption needs a timestamp column named by the
  manifest; there is no guessing from the rows
- a dotted cursor key carried literally on the row is found

Also: "[unserializable]" instead of "[object Object]" for a value JSON
cannot write, the contract comment names the timestamp case, and three
tests assert what they claim.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 18 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

The endpoint row is now the top strip of the table's main column: the
same box as the "Filters" header beside it, with a bottom border that
continues that header's.

The logs example draws row actions the way /drizzle does: the bulk bar
on a selection, and the row menu as a column that ships hidden. Actions
are drawn only for a manifest this site serves, because a root-relative
action href resolves against the page's origin whichever host the
manifest came from.

DataTableRemote stays independent of the actions block. It gains two
general props the host uses to place that block's pieces:

- extraColumns: columns appended after the manifest's
- defaultColumnVisibility: visibility defaults applied over the
  manifest's

The docs show the wiring.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/registry/src/components/data-table/data-table-remote/data-table-remote.tsx">

<violation number="1" location="packages/registry/src/components/data-table/data-table-remote/data-table-remote.tsx:381">
P2: `defaultColumnVisibility` is only used to initialize the persisted visibility state, so a newly added extra column can ignore its default and render visible. Merge defaults for previously unseen column IDs when columns or defaults change, or remount/reset visibility when the extra-column set changes.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread apps/web/src/app/headless/client.tsx Resolved
() => ({
...getDefaultColumnVisibility(definition),
...manifest.defaults?.columnVisibility,
...visibilityOverrides,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: defaultColumnVisibility is only used to initialize the persisted visibility state, so a newly added extra column can ignore its default and render visible. Merge defaults for previously unseen column IDs when columns or defaults change, or remount/reset visibility when the extra-column set changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/registry/src/components/data-table/data-table-remote/data-table-remote.tsx, line 381:

<comment>`defaultColumnVisibility` is only used to initialize the persisted visibility state, so a newly added extra column can ignore its default and render visible. Merge defaults for previously unseen column IDs when columns or defaults change, or remount/reset visibility when the extra-column set changes.</comment>

<file context>
@@ -358,8 +378,9 @@ function RemoteTable({
     () => ({
       ...getDefaultColumnVisibility(definition),
       ...manifest.defaults?.columnVisibility,
+      ...visibilityOverrides,
     }),
-    [definition, manifest.defaults?.columnVisibility],
</file context>

- data-table-remote: a filter under one of the pagination strategy's own
  keys (`offset`, or a custom cursor key) is dropped like the other
  reserved request parameters
- conformance kit: `readPath` reads own properties only, so a primary
  key of `toString` or `constructor` no longer looks present on every row
- headless page: this site's own endpoint pasted as an absolute URL is
  treated as served here, so it keeps its row actions and loses the
  "another host" note
- docs: the actions example carries its imports, and the sentence above
  it says the actions block is needed only when actions are drawn

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/app/headless/client.tsx">

<violation number="1" location="apps/web/src/app/headless/client.tsx:241">
P1: This trusts the requested origin rather than the manifest response's final origin. A same-origin URL that redirects to another host can supply root-relative action hrefs, which the page will post to this site's routes after `servedHere` enables them. Track and validate the final manifest response origin, or keep actions disabled unless that response is confirmed same-origin.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

if (externalHost === null) return true;
if (typeof window === "undefined") return false;
try {
return new URL(url).origin === window.location.origin;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This trusts the requested origin rather than the manifest response's final origin. A same-origin URL that redirects to another host can supply root-relative action hrefs, which the page will post to this site's routes after servedHere enables them. Track and validate the final manifest response origin, or keep actions disabled unless that response is confirmed same-origin.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/web/src/app/headless/client.tsx, line 241:

<comment>This trusts the requested origin rather than the manifest response's final origin. A same-origin URL that redirects to another host can supply root-relative action hrefs, which the page will post to this site's routes after `servedHere` enables them. Track and validate the final manifest response origin, or keep actions disabled unless that response is confirmed same-origin.</comment>

<file context>
@@ -229,14 +229,27 @@ function RemoteTable({
+    if (externalHost === null) return true;
+    if (typeof window === "undefined") return false;
+    try {
+      return new URL(url).origin === window.location.origin;
+    } catch {
+      return false;
</file context>

The serializer is optional, so the examples no longer pass it. Document the
reserved column keys, the conditions under which the demo draws actions, and
the new props in the skill and component catalog. List /headless in the docs
example lists, the install recipe's reading list and the home page.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="skills/data-table-filters/SKILL.md">

<violation number="1" location="skills/data-table-filters/SKILL.md:355">
P2: The actions bullet omits `getRowId` on `DataTableActionsProvider`, which is a required prop. Add `getRowId={(r) => r.uuid}` to the wrap instruction so the recipe typechecks.</violation>

<violation number="2" location="skills/data-table-filters/SKILL.md:355">
P2: `extraColumns={[createActionsColumn()]}` creates a new array on every render, despite the parenthetical saying it is stable. Define the actions column array at module scope and pass that constant, or the remote table rebuilds its columns on every render.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

- **Verify an endpoint** with `runEndpointConformance({ url, manifest })` and `formatConformanceReport`.
- **The query string is derived from the manifest.** `searchParamsSerializer` is optional; pass it only when the app already has one. A filterable column keyed `cursor`, `direction`, `size`, `sort`, `uuid`, `live` (or the pagination strategy's own key, e.g. `offset`) loses its filter with a console warning.
- **Slots can read the resolved manifest.** `commandSlot` and `toolbarActions` take a node or a function of `{ manifest, filterSchema, tableId, refresh, fetchPreviousPage? }`; `sheetSlot` receives the same context as its second argument.
- **Actions are the host's to draw.** The block does not depend on `data-table-actions`. Wrap it in `DataTableActionsProvider` with `manifest.actions`, pass `extraColumns={[createActionsColumn()]}` (a stable array) and put `DataTableActionsBar` in `floatingBarSlot`. Do this only for a manifest you serve yourself.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The actions bullet omits getRowId on DataTableActionsProvider, which is a required prop. Add getRowId={(r) => r.uuid} to the wrap instruction so the recipe typechecks.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At skills/data-table-filters/SKILL.md, line 355:

<comment>The actions bullet omits `getRowId` on `DataTableActionsProvider`, which is a required prop. Add `getRowId={(r) => r.uuid}` to the wrap instruction so the recipe typechecks.</comment>

<file context>
@@ -351,7 +350,10 @@ Key points:
-- **Verify an endpoint** with `runEndpointConformance({ url, manifest })` and `formatConformanceReport`.
+- **The query string is derived from the manifest.** `searchParamsSerializer` is optional; pass it only when the app already has one. A filterable column keyed `cursor`, `direction`, `size`, `sort`, `uuid`, `live` (or the pagination strategy's own key, e.g. `offset`) loses its filter with a console warning.
+- **Slots can read the resolved manifest.** `commandSlot` and `toolbarActions` take a node or a function of `{ manifest, filterSchema, tableId, refresh, fetchPreviousPage? }`; `sheetSlot` receives the same context as its second argument.
+- **Actions are the host's to draw.** The block does not depend on `data-table-actions`. Wrap it in `DataTableActionsProvider` with `manifest.actions`, pass `extraColumns={[createActionsColumn()]}` (a stable array) and put `DataTableActionsBar` in `floatingBarSlot`. Do this only for a manifest you serve yourself.
+- **Verify an endpoint** with `runEndpointConformance({ url, manifest })` and `formatConformanceReport`. `size` is a target: a group of rows sharing one timestamp cursor value may exceed it.
 
</file context>
Suggested change
- **Actions are the host's to draw.** The block does not depend on `data-table-actions`. Wrap it in `DataTableActionsProvider` with `manifest.actions`, pass `extraColumns={[createActionsColumn()]}` (a stable array) and put `DataTableActionsBar` in `floatingBarSlot`. Do this only for a manifest you serve yourself.
- **Actions are the host's to draw.** The block does not depend on `data-table-actions`. Wrap it in `DataTableActionsProvider` with `manifest.actions` and `getRowId={(r) => r.uuid}`, pass `extraColumns={[createActionsColumn()]}` (a stable array) and put `DataTableActionsBar` in `floatingBarSlot`. Do this only for a manifest you serve yourself.

- **Verify an endpoint** with `runEndpointConformance({ url, manifest })` and `formatConformanceReport`.
- **The query string is derived from the manifest.** `searchParamsSerializer` is optional; pass it only when the app already has one. A filterable column keyed `cursor`, `direction`, `size`, `sort`, `uuid`, `live` (or the pagination strategy's own key, e.g. `offset`) loses its filter with a console warning.
- **Slots can read the resolved manifest.** `commandSlot` and `toolbarActions` take a node or a function of `{ manifest, filterSchema, tableId, refresh, fetchPreviousPage? }`; `sheetSlot` receives the same context as its second argument.
- **Actions are the host's to draw.** The block does not depend on `data-table-actions`. Wrap it in `DataTableActionsProvider` with `manifest.actions`, pass `extraColumns={[createActionsColumn()]}` (a stable array) and put `DataTableActionsBar` in `floatingBarSlot`. Do this only for a manifest you serve yourself.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: extraColumns={[createActionsColumn()]} creates a new array on every render, despite the parenthetical saying it is stable. Define the actions column array at module scope and pass that constant, or the remote table rebuilds its columns on every render.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At skills/data-table-filters/SKILL.md, line 355:

<comment>`extraColumns={[createActionsColumn()]}` creates a new array on every render, despite the parenthetical saying it is stable. Define the actions column array at module scope and pass that constant, or the remote table rebuilds its columns on every render.</comment>

<file context>
@@ -351,7 +350,10 @@ Key points:
-- **Verify an endpoint** with `runEndpointConformance({ url, manifest })` and `formatConformanceReport`.
+- **The query string is derived from the manifest.** `searchParamsSerializer` is optional; pass it only when the app already has one. A filterable column keyed `cursor`, `direction`, `size`, `sort`, `uuid`, `live` (or the pagination strategy's own key, e.g. `offset`) loses its filter with a console warning.
+- **Slots can read the resolved manifest.** `commandSlot` and `toolbarActions` take a node or a function of `{ manifest, filterSchema, tableId, refresh, fetchPreviousPage? }`; `sheetSlot` receives the same context as its second argument.
+- **Actions are the host's to draw.** The block does not depend on `data-table-actions`. Wrap it in `DataTableActionsProvider` with `manifest.actions`, pass `extraColumns={[createActionsColumn()]}` (a stable array) and put `DataTableActionsBar` in `floatingBarSlot`. Do this only for a manifest you serve yourself.
+- **Verify an endpoint** with `runEndpointConformance({ url, manifest })` and `formatConformanceReport`. `size` is a target: a group of rows sharing one timestamp cursor value may exceed it.
 
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/content/docs/15-headless.mdx">

<violation number="1" location="apps/web/src/content/docs/15-headless.mdx:464">
P3: This unconditionally requires CORS, but same-origin endpoints such as the two examples do not need CORS headers. Qualify the requirement to cross-origin endpoints so users do not add unnecessary CORS configuration.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

| `/drizzle/api/schema` | The [/drizzle](/drizzle) logs table: Postgres, facets, counts, chart, live mode |
| `/headless/api/orders/schema` | An in-memory array as plain JSON: inferred schema, offset paging, no chart |

To try your own endpoint, paste its manifest URL. It needs CORS headers.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: This unconditionally requires CORS, but same-origin endpoints such as the two examples do not need CORS headers. Qualify the requirement to cross-origin endpoints so users do not add unnecessary CORS configuration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/web/src/content/docs/15-headless.mdx, line 464:

<comment>This unconditionally requires CORS, but same-origin endpoints such as the two examples do not need CORS headers. Qualify the requirement to cross-origin endpoints so users do not add unnecessary CORS configuration.</comment>

<file context>
@@ -453,13 +453,19 @@ contract itself.
+| `/drizzle/api/schema`         | The [/drizzle](/drizzle) logs table: Postgres, facets, counts, chart, live mode |
+| `/headless/api/orders/schema` | An in-memory array as plain JSON: inferred schema, offset paging, no chart      |
+
+To try your own endpoint, paste its manifest URL. It needs CORS headers.
+
+**Inspect** lists what the endpoint declares and runs
</file context>
Suggested change
To try your own endpoint, paste its manifest URL. It needs CORS headers.
To try your own endpoint, paste its manifest URL. A cross-origin endpoint needs CORS headers.

SWC drops the leading space of a multi-line JSX text that contains an
HTML entity, so "Headless tables" ran into "rendered". Use a literal
apostrophe instead of &apos;.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/src/content/docs/14-actions.mdx">

<violation number="1" location="apps/web/src/content/docs/14-actions.mdx:363">
P3: `/headless` does not always render these two actions from the manifest alone: it requires a same-site manifest that advertises actions, and the public `/drizzle` manifest omits them by default. Qualify this sentence with those conditions.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

When actions are off, the list endpoint does not advertise them, the UI
renders no buttons, and `POST /drizzle/api/actions/*` answers 403.

[/headless](/headless) renders the same two actions from the manifest alone.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: /headless does not always render these two actions from the manifest alone: it requires a same-site manifest that advertises actions, and the public /drizzle manifest omits them by default. Qualify this sentence with those conditions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At apps/web/src/content/docs/14-actions.mdx, line 363:

<comment>`/headless` does not always render these two actions from the manifest alone: it requires a same-site manifest that advertises actions, and the public `/drizzle` manifest omits them by default. Qualify this sentence with those conditions.</comment>

<file context>
@@ -341,12 +341,24 @@ control — see [Headless Tables](/docs/headless#trust-boundary).
+When actions are off, the list endpoint does not advertise them, the UI
+renders no buttons, and `POST /drizzle/api/actions/*` answers 403.
+
+[/headless](/headless) renders the same two actions from the manifest alone.
+See [Headless Tables](/docs/headless#capabilities) for the wiring.
</file context>
Suggested change
[/headless](/headless) renders the same two actions from the manifest alone.
[/headless](/headless) renders these actions when a same-site manifest advertises them.

@mxkaske
mxkaske merged commit 06a54bc into main Nov 9, 2026
20 checks passed
@mxkaske
mxkaske deleted the feat/headless-demo branch November 9, 2026 17:40

This branch was successfully deployed

1 active deployment
Preview — 9606febc Deployed Nov 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant