# Under the hood

> A page is a declarative document validated against one schema and rendered by a native player - with data reaching it only through the engine's contract, which is what makes access rules hold on a screen

Source: https://aipril.dev/docs/cms/under-the-hood

## A page is a document, not a program

A page is a **declarative description in JSON**: which widgets, in what arrangement, with what
properties, bound to what.

That is the decision everything else on this page follows from. A page that is a program can only be
understood by running it; a page that is a document can be validated, diffed, generated, reviewed
and rolled back.

```mermaid
flowchart TD
    DESC["Page description<br/>widgets, layout, props, bindings"] --> VAL["One validation schema"]
    VAL --> PLAYER["Native player<br/>renders the description"]
    PLAYER --> W["Widget catalogue<br/>declared props and events"]
    PLAYER -->|"/api/query, /models"| ENG["Query engine"]
    ENG --> ACC["Access decided here:<br/>row rules, column masks"]
    PLAYER --> COND["Predicate engine<br/>conditional visibility"]
```

**One schema validates all of it**, not a validator per widget maintained by whoever added that
widget. So a malformed page fails at the moment it is written instead of as an empty rectangle in
front of a user, and an agent writing pages cannot invent a property that does not exist.

**And it is rendered by a native player**, not by generating markup ahead of time. The description
arrives, the player draws it. That is what allows the same description to be edited on a canvas and
shown to a user without two code paths that disagree.

## Widgets are declared

Every widget in the catalogue declares its contract: the properties it accepts with their types, the
events it emits, what it can be bound to.

Three consequences, and they are the reason the catalogue can be large without collapsing:

- **The editor is generic.** The properties panel is generated from the declaration instead of
  hand-built per widget, so adding a widget does not mean building its editor as well.
- **The agent works from the same declaration.** It is not trained on your widget set; it reads it.
  A widget added last week is available to it immediately, with its real properties.
- **Validation is uniform.** One mechanism checks every widget, so a widget nobody has used in a
  year is as safely configured as the popular ones.

## Data reaches a page one way

A page has **no direct path to storage**. Everything it shows comes through the query engine's
contract - the same `/api/query` and `/models` used by dashboards, MCP tools and
AI employees.

That is what makes access control real on a screen. A widget does not fetch rows; it asks a question, and the engine answers it for the person looking - with row
policies and column masks compiled into the query plan. There is no page-level permission model
maintained beside the data-level one, because there is no second path for a page to get data
through.

See [Data engine](https://aipril.dev/docs/engine.md) and
[Access control in the query plan](https://aipril.dev/docs/engine/under-the-hood.md).

## Conditional visibility uses the process engine's predicates

Whether a block is shown is a condition, and it is evaluated by the **same predicate engine that
decides process transitions**.

The same one, not a similar one, so "show the approve button when the request is pending and the
viewer may approve" and "allow this transition when the request is pending and the actor may
approve" are expressed identically and cannot drift into two dialects with different edge cases.

## Charts come from one dispatcher

A chart on a page is not a page-specific chart implementation. The same dispatcher that draws charts
on a dashboard draws them here: builders return data structures, components render them, and adding
a chart type is one entry in one place instead of an edit to every surface that shows charts.

So a chart behaves the same wherever it is mounted - including its drill-down, which lives in the
chart component instead of whatever is wrapping it. See
[Analytics under the hood](https://aipril.dev/docs/analytics/under-the-hood.md).

## Themes: a design system, not per-page styling

Appearance is not a property of a page. It lives in a **theme**, held per organisation, and every
page reads from it.

A theme carries four kinds of thing, and the split is what makes it usable and not a colour picker:

- **Tokens** - the palette and the values everything else is expressed in. A widget does not carry a
  hex code; it refers to a token. Change the token and every widget that used it changes, including
  ones nobody thought about at the time.
- **Typography** - the families, the scale, the weights and the rhythm, so headings are consistent
  across pages built by different people in different months.
- **Density** - how much air the interface has. The same screens for an operator on a tablet and an
  analyst on a wide monitor should not be the same density, and that is one setting instead of a
  hundred spacing decisions.
- **Component-level overrides** - where a specific component should look different in your brand,
  that is expressed once, in the theme, instead of as a property set on every instance of it.

**Light and dark are part of the theme**, not a second theme to maintain in parallel: the dark
palette is defined alongside the light one, so a page authored once works in both and a viewer can
switch.

### What that lets you do

Concretely, because "themeable" is a word every product uses:

- **Put your brand book in once.** Primary, accent, surface, border, the states - defined as tokens,
  and every widget that ever used them follows, including ones added later.
- **Change the shape of things globally.** Corner radius, border weight, shadow depth, the scale of
  spacing. A platform that looks sharp and dense, or soft and airy, is a handful of values, not a
  redesign.
- **Set your type once.** The families, the sizes, the weights and the line heights - so a heading
  on a portal page and a heading in an application are the same heading.
- **Give the warehouse tablet a compact density** and the analyst's monitor a comfortable one,
  without either being a compromise or a separate set of pages.
- **Ship a dark palette for the people who need one** - a control room, a night shift, a person who
  simply prefers it - from the same page definitions.
- **Override one component where your brand demands it.** Your buttons are square and your inputs
  are not: said once, in the theme, instead of as a property on four hundred instances.
- **Run more than one theme.** The employee portal in your corporate identity, a partner-facing site
  in a lighter variant, a campaign microsite in its own - all from one design system, so they are
  variants and not separate front-ends.
- **Try it before committing.** Themes are previewed as they are edited, so "what would the whole
  platform look like in this palette" is a question you answer by looking.

**And it degrades safely.** A theme that carries only a palette - which is what the built-in ones
were before tokens existed - still renders correctly: the player falls back to its own defaults for
anything the theme does not specify. Adding detail to a theme is additive, not a migration.

Themes are edited in their own editor, with a tab per concern - colours, typography, density,
components - and previewed rather than imagined. Several ship with the platform as starting points,
and an organisation authors its own.

The reason this is worth the machinery: **consistency stops depending on discipline.** Forty pages
built by six people over two years are consistent because they all resolve the same tokens, not
because everybody remembered the same hex code.

## The editor

A canvas over the same description: select a widget, move it, resize it, snap to the grid, edit its
properties in a generated panel, undo and redo. Selection, history and scaling are the editor's own
concerns; what it produces is the ordinary page description, so nothing about a page reveals whether
it was drawn, generated or hand-written.

The AI editing path is the same surface rather than a parallel one: the assistant is handed the
selected widget's type, its editable properties and their current values, and returns a small set of
property edits. It changes what you would have changed, in the panel you would have changed it in.

## Navigation binds to data too

Menus, side navigation, breadcrumbs, the app switcher and mega-menus can take their items from a
**master-data entity** instead of from a hardcoded list. A menu of product categories is the
category list; add a category and the menu has it.

See [Master data](https://aipril.dev/docs/master-data.md).

## What this means for an application

A page is an artifact of an application, like the data model and the processes: written into the
repository, versioned with everything else, installed by the reconciler, and immutable once a
version is published.

So a screen somebody rearranged is part of the release instead of a change made in production that
nobody can account for - and moving an application to another organisation brings its interface with
it. See [Applications](https://aipril.dev/docs/apps/under-the-hood.md).
