# Dashboard Widgets

Put the numbers your team actually checks on the admin dashboard.

The admin dashboard is built from widgets. Each one declares what it wants to know, and the
system runs that query on its behalf, scoped to the permissions of whoever is looking.

## Availability

|                            |                                                                                                                                                          |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Built in                   | The widget system, the built-in sources and admin rendering all ship                                                                                     |
| Plugin-contributed widgets | **Experimental.** They render, but the contribution shape is still settling — see the [stability ladder](https://nextlyhq.com/docs/plugins/stability.md) |
| Permission                 | A widget's query runs scoped to the viewer, so two people can see the same dashboard and different numbers                                               |

## Archetypes

A widget declares an archetype rather than drawing itself, so dashboards stay visually
consistent no matter who contributed the widget:

| Archetype | Shows                                          |
| --------- | ---------------------------------------------- |
| `metric`  | One number                                     |
| `stats`   | Several labelled, clickable numbers            |
| `table`   | Rows and columns                               |
| `list`    | An ordered list of items                       |
| `actions` | Buttons for things to do                       |
| `text`    | Prose you wrote, as markdown                   |
| `custom`  | Your own component, when none of the above fit |

A `text` widget is its prose. It declares `content` instead of a query, and the host draws
it: headings, paragraphs, lists, emphasis, inline code, block quotes and links.

```ts
{
  id: "acme/release-notes",
  title: "Before you release",
  archetype: "text",
  defaultSize: "md",
  content: [
    "## Checklist",
    "",
    "Drain the queue, then read [the runbook](https://example.com/runbook).",
  ].join("\n"),
}
```

Three things the markdown cannot do, by design. Raw HTML is shown as the text it is. A link
may point at `http`, `https`, `mailto`, `tel`, or a path on this site written as `/...`,
`./...` or `#...`; a link to anything else — a bare `posts?status=draft`, an address with no
scheme, or a path a browser would read as another site, such as `/\example.com` — is left on
screen as the markdown you wrote it in, so you can see it was refused. And the
content is bounded — it travels inside every dashboard load for every reader who is offered
the card, so it is for notes and a pointer to a runbook, not for the runbook itself. An
external link opens in a new tab.

Sizes are `sm`, `md`, `lg`, `xl` and `full`; heights are `short` and `tall`. On the dashboard,
cards sit in one ordered list, filling rows left to right and wrapping, and each card is one of
five widths: `sm` is a quarter of the row, `md` a third, `lg` a half, `xl` two thirds and `full`
the whole row. A card starts at the widget's `defaultSize`, and a reader can change its width from
the card's menu to any size between the widget's `minSize` and `maxSize` (every size, when the
widget declares neither). Widths follow the dashboard's own width rather than the window's: below
1,024 pixels a quarter, third or half card is drawn at half width and a wider one at full width,
and below 640 pixels every card is full width. Below the admin's `md` breakpoint the dashboard is
read-only: arranging it is offered from `md` up, and the arrangement made there applies at every
width.

## Declared, not fetched

A widget does not run its own database query. It **declares** one, and the system executes
it. That is what makes permission scoping reliable rather than something each widget author
has to remember: a widget cannot read what its viewer could not have read directly.

It also means most widgets need no data-fetching code at all.

### Sources

A query names a source, and the source decides what the query may ask:

| Source              | Reads                                                                                                       | Supports                                                                                                                                  |
| ------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `collection:<slug>` | The collection's entries, through the same access-controlled read as the API                                | `count`, `list`, `groupBy`, and `timeseries` where the collection has a usable date                                                       |
| `single:<slug>`     | The single's one document, as a list of one row                                                             | `list` only — a single answers a fixed question, so a `where`, a `sort`, a `count` or a bucketing over it is refused rather than answered |
| `system:…`          | A core service — versions, releases, activity and the like — each answering through its own service's rules | Whatever that source declares                                                                                                             |

Every collection and every single the install has is published as a source, and the dashboard
offers a card derived from each: a count, a recent list, a table and a health breakdown per
collection, and one status card per single showing whether it is published and when it last
changed. Offered, never placed — nobody's dashboard gains a card because a collection was
created — and only to a reader who may read that collection or single.

> [!WARNING]
> Contributing a widget from a plugin works today, but `PluginAdminWidget` is marked
> `@experimental` and its shape may change before it settles. Building on it is reasonable;
> depending on it in something you cannot easily update is not.

## Related

* [Plugin admin UI](https://nextlyhq.com/docs/plugins/admin-ui.md) — contributing admin surfaces
* [Plugin stability](https://nextlyhq.com/docs/plugins/stability.md) — what is settled and what is not
