# Dashboards

> Compose dashboards from a 10-widget library, pin one as the org default, and clone presets to build your own — all gated by per-policy RBAC.

Source: https://zop.dev/developer-docs/operations/dashboards

---

Compose your own dashboards from a 10-widget library and pin one as the org default. Dashboards are owned by the organization — anyone with the right role sees the same set.

## System Presets

Four presets ship out of the box. Presets are read-only — you can clone any preset to create your own dashboard, but the originals never change. Presets are served from memory and are never persisted to the database.

| Preset | Audience | Default widgets |
|---|---|---|
| `executive` | Leadership / FinOps leads | Stat cards, cost trend, top costly resources, recommendations |
| `engineering` | Platform & SRE teams | Schedule execution, upcoming actions, recommendations, cloud spend |
| `finops` | Finance partners | Showback by team, cost trend, budget health, top costly resources |
| `all_widgets` | Everyone | All 10 widgets, useful as a starting point for clones |

## Widget library

Every dashboard is a layout over the same 10 widgets. Each widget declares its required policy and is rendered through an error boundary — a widget you can't see, or a widget that throws, becomes a placeholder instead of unmounting the page.

| Widget type | What it shows | Required policy |
|---|---|---|
| `stat_cards` | Compact key-metric tiles (cost, savings, anomalies, resources) | `dashboard:view` |
| `cost_trend` | Daily/weekly/monthly cost line chart | `reports:view` |
| `cloud_spend` | Per-provider spend breakdown | `reports:view` |
| `top_costly` | Top N costly resources | `reports:view` |
| `recommendations` | Open recommendations + savings | `recommendation:view` |
| `quick_insights` | Curated callouts (idle/orphan/anomalies) | `reports:view` |
| `budget_health` | Budget status across teams | `reports:view` |
| `showback` | Cost attribution by team or tag | `reports:view` |
| `schedule_execution` | Recent scheduler executions | `schedule:view` |
| `upcoming_actions` | Next 24h scheduled start/stop actions | `schedule:view` |

## Bounds

| Bound | Limit | On exceed |
|---|---|---|
| Dashboards per org | `50` | 409 — code: DASHBOARD_LIMIT_EXCEEDED |
| Widgets per dashboard | `50` | 400 on save |
| Widget config size | `4 KiB` | 400 on save |
| Layout payload size | `64 KiB` | 400 on save |
| Name uniqueness | `Per-org (active rows)` | 409 — code: DUP_NAME |

## RBAC

Four policies gate dashboard operations:

- `dashboard:view` — read dashboards and presets
- `dashboard:create` — create new dashboards (counts against the org's 50-dashboard quota)
- `dashboard:update` — rename, edit layout, change widgets, set default
- `dashboard:delete` — delete a user-created dashboard (presets are immutable)

## API

### List dashboards

### GET /dashboards

List dashboards visible to the caller, plus the org

Query: `?limit=20&offset=0&sort_by=name&sort_order=asc`

```json title="Response"
{
  "data": [
    { "id": "executive", "name": "Executive", "kind": "preset" },
    { "id": "engineering", "name": "Engineering", "kind": "preset" },
    { "id": "finops", "name": "FinOps", "kind": "preset" },
    { "id": "all_widgets", "name": "All Widgets", "kind": "preset" },
    {
      "id": "01HF8K9C5G4M2N3P4Q5R6S7T8U",
      "name": "Platform Team",
      "kind": "user",
      "createdAt": "2026-04-26T10:30:00Z",
      "updatedAt": "2026-04-27T08:00:00Z"
    }
  ],
  "defaultDashboard": "executive"
}
```

### Get dashboard

### GET /dashboards/{id}

Get a dashboard by id. Preset keys are served from memory; UUIDs hit the dashboards table.

### Create dashboard

### POST /dashboards

Create a new user-owned dashboard. Cloning a preset is a client-side operation: GET the preset, modify, POST.

```json title="Request"
{
  "name": "Platform Team",
  "layout": { "rows": [[{ "type": "stat_cards", "size": "wide" }]] },
  "widgets": [
    { "type": "stat_cards", "config": {} }
  ]
}
```

### Update dashboard

### PATCH /dashboards/{id}

Partial update — name, layout, or widgets. All-nil bodies return 400. PATCH on a preset id returns 405 (ErrPresetImmutable).

### Set default dashboard

### PUT /dashboards/default

Set the org

```json title="Request"
{ "id": "executive" }
```

Send `{ "id": "" }` to clear the pointer — frontend falls back to `all_widgets`.

### Delete dashboard

### DELETE /dashboards/{id}

Delete a user-owned dashboard. Preset keys return 405. If the deleted dashboard is the org default, the pointer is cleared in the same transaction.

**Warning**

Deleting an organization soft-deletes all of its dashboards via the organisation cascade — no orphaned rows.

See [Roles & Permissions](https://zop.dev/docs/administration/roles-permissions) for how dashboard policies compose with the rest of the policy table.
