Skip to content

Dashboard

The home dashboard is a grid of SQL-defined panels declared in config/dashboard.hcl. Every panel's SQL runs read-only, so the dashboard can never mutate your data.

hcl
# config/dashboard.hcl
columns = 4

panel {
  type          = "stat"
  label         = "Orders 30d"
  category      = "Sales"
  sql           = "SELECT count(*) AS v FROM orders WHERE placed_at > now() - interval '30 days'"
  compare_sql   = "SELECT count(*) AS v FROM orders WHERE placed_at BETWEEN now() - interval '60 days' AND now() - interval '30 days'"
  compare_label = "prev 30d"
  spark         = "SELECT count(*) AS v FROM orders WHERE placed_at > now() - interval '30 days' GROUP BY date_trunc('day', placed_at) ORDER BY date_trunc('day', placed_at)"
  good_when     = "up"
}

Grid

KeyTypeDescription
columnsnumberNumber of grid columns the dashboard lays out.

Panels flow into the grid in declaration order. Each may set its own w (column span) and h (row span), and a category used to group panels under headings.

Panel types

Set type to one of stat, chart, table, iframe.

stat — a single number

A big-number tile with an optional period-over-period comparison and an inline sparkline.

hcl
panel {
  type          = "stat"
  label         = "Revenue 14d"
  category      = "Revenue"
  format        = "money"
  sql           = "SELECT coalesce(sum(total),0) AS v FROM orders WHERE placed_at > now() - interval '14 days'"
  compare_sql   = "SELECT coalesce(sum(total),0) AS v FROM orders WHERE placed_at BETWEEN now() - interval '28 days' AND now() - interval '14 days'"
  compare_label = "prev 14d"
  spark         = "SELECT coalesce(sum(total),0) AS v FROM ... GROUP BY date_trunc('day', placed_at) ORDER BY 1"
  good_when     = "up"
  alert_above   = 20
}
KeyDescription
sqlReturns a single numeric column v — the headline value.
formatnumber, money, percent, or duration.
compare_sqlA second v query for the comparison baseline; the tile shows the delta.
compare_labelLabel for the comparison period (e.g. prev 24h).
sparkA query returning an ordered series of v values, drawn as an inline sparkline.
good_whenWhich delta direction is favorable: up (default) paints a rising value green; down paints a falling value green (for errors, latency, …).
alert_above / alert_belowThresholds that flag the tile as critical when the value rises above / falls below them.

chart — a time or category series

hcl
panel {
  type     = "chart"
  label    = "Revenue per day (30d)"
  category = "Trends"
  format   = "money"
  chart    = "area"          # "line" | "bar" | "area"
  sql      = "SELECT date_trunc('day', placed_at) AS t, coalesce(sum(total),0) AS v FROM orders WHERE placed_at > now() - interval '30 days' GROUP BY 1 ORDER BY 1"
}
KeyDescription
chartChart kind: line, bar, or area.
sqlReturns t (the x label/timestamp) and v (the numeric value) per row.
formatValue formatter for axes and tooltips.

table — a live query as rows

hcl
panel {
  type     = "table"
  label    = "Past-due subscriptions"
  category = "Attention"
  link     = "subscriptions"   # rows deep-link into this table's records
  roles    = ["support"]
  sql      = "SELECT id, customer_id, product_id, status, renews_at FROM subscriptions WHERE status = 'past_due' ORDER BY renews_at NULLS FIRST LIMIT 10"
}
KeyDescription
sqlThe rows to display; column set is taken from the query.
linkA table name — each row links to that table's matching record.

A table panel can style its columns with repeated field { } blocks; without them the column set comes straight from the query and cells fall back to sensible defaults (headers humanized, ISO timestamps date-formatted):

hcl
panel {
  type  = "table"
  label = "Top products"
  sql   = "SELECT name, revenue, status FROM …"

  field {
    key     = "revenue"
    format  = "money"
    align   = "right"
    display = "bar"          # in-cell data bar ("heat" tints by magnitude)
  }
  field {
    key   = "status"
    badge = { active = "green", churned = "red" }
  }
}

field keys: key (required), label, format (money/percent/number/ bytes/duration/date/datetime/rel, plus the aliases currency/pct/ num/dur — validated at load), align, max, badge (value → tone map), display (bar | heat), tone (hue for badge-less dataviz: accent default, or green/red/orange/blue/violet).

iframe — an embedded view

hcl
panel {
  type  = "iframe"
  label = "Grafana"
  url   = "https://grafana.example/d/abc"
}

iframe panels require a url instead of sql.

Common panel keys

KeyApplies toDescription
typeallstat, chart, table, iframe. Required.
labelallThe panel's title. Required.
idallOptional stable id; when omitted the panel is identified by its position.
categoryallHeading this panel groups under.
w / hallColumn / row span in the grid.
rolesallRestrict the panel to these roles. Omit → visible to all who can see the dashboard.
linktableTarget table for row links.
urliframeThe embedded URL.

Template variables

Declare a variable once and every panel that references it grows a live selector at the top of the dashboard — flip it and the whole grid re-queries in place. The selection lives in the URL, so "the dashboard, last 90 days" is a link you can send to a teammate.

Mechanically: placeholders work in every panel's sql, compare_sql and spark. They reference the global template variables declared in a variables.hcl, and are resolved per request from v_<name> URL parameters (falling back to each variable's default) and bound as SQL parameters — never string-spliced.

hcl
panel {
  type  = "stat"
  label = "Orders"
  sql   = "SELECT count(*) AS v FROM orders WHERE placed_at > now() - {{window}} * interval '1 day'"
}

Whenever any variables are in scope, the dashboard renders a selector bar above the grid — a segmented control when a variable has up to six options, a select otherwise. Changing a value re-runs every panel that references it and updates the URL (?v_window=90). A supplied value outside a variable's static option set is a hard 400.

The common case has a shorthand — type = "window" declares a ready-made time-window selector (7/30/90 days, default 30, label "Window", int semantics), each part overridable by declaring it yourself:

hcl
# variables.hcl
variable "days" {
  type = "window"
}
sql
-- any panel:
WHERE placed_at > now() - {{days}} * interval '1 day'

Safety

Every dashboard and panel query runs in a read-only transaction with a 5-second statement timeout and hard row caps (500 chart points, 100 sparkline points, 50 table rows). The visual dashboard editor additionally offers a preview that runs a panel through the same read-only path and returns the rendered result without writing anything to config. Like all config, the dashboard is versioned — see Architecture.

Released under the MIT License.