How it renders

The manifest → surface pipeline in Commerce Studio — why an app ships no frontend code, what the renderer handles for you, and where a view's reads and writes actually go.

Commerce Studio is a rendering engine. Understanding the pipeline is what makes the rest of this group obvious rather than arbitrary.

The pipeline

text
                  every installed app's cockpit.json
                                 │
                     ┌───────────┴───────────┐
                     │   merge + resolve     │  group, position, id/parent anchors
                     └───────────┬───────────┘
                                 │
        ┌────────────────────────┼────────────────────────┐
        ▼                        ▼                        ▼
    sidebar                 live routes             dashboard cards
  (navigation[])              (views[])                (widgets[])
                                 │
                  ┌──────────────┼──────────────┐
                  ▼              ▼              ▼
              list renderer  detail renderer  form renderer
                  │              │              │
                  └──────────────┴──────────────┘
                                 │
                  reads/writes the entity directly
                          — unless a view declares `write`,
                            which dispatches to your own capability

Three consequences follow, and they explain most of the design:

  1. There is no component to ship, because there is no place to put one. The renderers belong to the studio.
  2. You declare where things go, not how they look. Your app's screens and a dozen other apps' screens have to end up in one coherent sidebar, and no app is in a position to arrange that on its own.
  3. The manifest is read per request. Editing it and reloading is the whole development loop; there is no build step.

What the renderer handles

Everything in this list is the studio's job and none of it is yours:

ConcernHandled by
Authentication and tenant contextThe studio, from the operator's session
Reading the entity, pagination, total countsThe list renderer
Sorting, filter state, URL stateThe list renderer
Loading, empty and error statesEvery renderer
Form controls per field type, client-side validationThe form renderer
Optimistic concurrency, where declaredThe detail and form renderers
Localised labels, per the operator's UI localeEvery renderer
Layout, spacing, typography, dark modeThe Studio design system

That is why the renderers get all of it right in one place instead of in thirteen apps.

Where a view's data comes from

By default the renderer reads and writes the entity directly. That is the right default for an admin surface: a position column, a label, a boolean flag gain nothing from a round trip through your function that would write the same column back.

Declare write on a view when the database cannot do the job on its own — a contact that must also become a platform user, a stock movement that has to leave a ledger entry behind it:

cockpit.json
"write": { "method": "POST", "endpoint": "/suppliers" }

That dispatches the save to your app's own capability at <gateway>/v1<endpoint>, with the operator's Bearer token, X-Revenexx-Tenant and X-Revenexx-Market. See Actions and writes.

The three view types, and only three

text
list      detail      form

There is no chart type, no board type, no custom type. A view declares one of the three and configures the renderer that goes with it.

Every shipped app declares the same four-view set per entity:

cockpit.json
"views": [
  { "route": "/suppliers",          "type": "list",   "entity": "suppliers", "columns": [] },
  { "route": "/suppliers/:id",      "type": "detail", "entity": "suppliers" },
  { "route": "/suppliers/new",      "type": "form",   "entity": "suppliers", "mode": "create" },
  { "route": "/suppliers/:id/edit", "type": "form",   "entity": "suppliers", "mode": "edit" }
]

Two route conventions, and they differ

This trips people up, so it is worth stating plainly:

WherePlaceholderExample
A cockpit.json route:param/suppliers/:id/edit
A capability route in manifest.capabilities.json{braces}POST /suppliers/{id}/deactivate

A view's route is a Cockpit route. An action's or a write's endpoint is your capability's path — and its :param placeholders are substituted from the route params and the record id before the request goes out.

Routes are mounted under the studio that hosts the app, which for these views is /commerce, so "/suppliers" becomes /commerce/suppliers in the browser. Declare the studio-relative path.

Composition across apps

The studio reads every installed app's manifest and merges the declarations. Sidebar entries from all apps are ordered together by group and position; every views entry becomes a live route; every widgets entry becomes available on the dashboard.

That is also why an app can attach a page under another app's heading rather than claiming its own top-level entry, using id and parent anchors — see Navigation.

When declaring is not enough

The renderers cover an admin surface, not every surface. Two honest boundaries:

  • Filters have four shapes in production use. If an operator needs to slice data a way they cannot express, that is a capability answering a question, not a filter. (GET /v1/products/grid and POST /v1/prices/resolve exist for exactly that reason.)
  • A widget has two renderers. EntityCount and EntityList. There is no chart widget.

Where the declarative surface runs out, the answer is a capability your function serves and an api action that calls it — not a component.

Where to go next

Was this page helpful?