Charts with dbt Charts
Status: proposal, spike run once by hand (2026-09-17,
dct0.8.0). Nothing is built into the service. The spike showed that option 1 below works, and only with a gate in front ofdct: on its own it renders numbers a model typed in and makes outbound requests a board asks for. The spike is not a CI check, so nothing here is witnessed.
dbt Charts is fine as a chart format and a renderer. It becomes a problem as soon as it queries the warehouse itself: that is a second route to the data, and it does not pass through anything this service enforces.
What it is
Section titled “What it is”dbt Charts is an Apache-2.0, declarative chart language released in September 2026.
| Part | What it does |
|---|---|
| One YAML file | holds the queries (SQL), the charts (type, query, encodings), variables, and Markdown with Jinja |
charts/ |
sits beside dbt models/ and may use ref(). A dbt project is not required. |
dct validate |
checks a file and returns warnings an agent can act on (docs) |
dct render / dct serve |
produces SVG, HTML, PNG, PDF or terminal output |
| Data access | source types in 0.8.0: dbt_profile, postgres, snowflake, bigquery, redshift, mysql, trino, duckdb, sqlite, csv, json, parquet, http (PyPI). A warehouse login is a dbt profile or environment variables: one saved credential. |
| Query types | sql, http (any URL), values (rows written into the YAML), schema |
| dbtCharts.com | a hosted service (public beta) where viewers do not need a warehouse login |
Fabric/TDS is not among its source types. Databricks is not a direct type
either; it may be reachable through dbt_profile, which the spike did not try.
Why it must not fetch the data
Section titled “Why it must not fetch the data”When dct runs a chart’s SQL, it connects with one saved login. Every control
in 05 and 19 lives on the
other path:
| Control | If dct queries the warehouse |
|---|---|
| Entra OBO: the query runs as the caller | gone; every query is the saved login |
| Fabric row- and column-level security | not applied, and Fabric is not supported anyway |
deny_tagged column rules from OpenMetadata |
skipped; they are enforced in the executor, not in dct |
DAS_ACCESS_RULES and gateway roles |
skipped; the call never passes through APIM |
| dbtCharts.com, “viewers need no warehouse login” | exposes the saved login’s data to anyone who can open the chart |
A chart that queries on its own is, by definition, authz_tier: service, and
for a source that is user tier it is a bypass. So dct may draw data. It
may not fetch it.
Where it fits
Section titled “Where it fits”1. Charts in chat (recommended)
Section titled “1. Charts in chat (recommended)”caller ──run_query──▶ executor (OBO, deny_tagged, access rules) │ rows, already governed ▼ result.json ──▶ dct validate ──▶ dct render ──▶ SVG ▲model ──chart YAML──▶ gate ┘ (charts and layout only; no SQL, no data, no URLs)- The executor runs the query exactly as it does today, as the caller.
- The rows it returns are written to a local file, and the chart reads that
file, never the warehouse.
dctholds no credentials. - The model writes only the chart specification, and a gate enforces
that;
dctdoes not (see below). What the chart shows is then the governed result, not anything the model typed. dct validateanddct renderboth fail closed. A chart that fails either is not shown, and the error goes back to the model.validatealone is not enough: it does not check column names or whether a named source exists.- It works for every source, Fabric included, because
dctnever connects to any of them. - A column withheld by
deny_taggedcannot appear in the chart: it never reaches the file.
The chart would be an SVG attached to a run_query answer, or an extra event
on the ask service stream.
2. A fourth publishing target: not now
Section titled “2. A fourth publishing target: not now”A publisher/targets/dbtcharts.py beside Power BI, Superset and Tableau
would turn a Plan into dbt Charts YAML. The output is plain, deterministic
text, which suits the golden cases in publisher/contract/, and nothing in
it would be generated by a model (14).
It fails the first filter in
15:
OpenMetadata’s dashboardServiceType has no dbt Charts entry. Only
CustomDashboard would take it, which records a dashboard without the
lineage a promoted number exists to carry.
It also fails on authorisation. A published chart is rendered later, by whoever opens it, with the saved login. That makes it service tier on every source, and impossible on Fabric. Revisit it if OpenMetadata adds the service type and there is a service-tier source where that is acceptable.
3. dbtCharts.com: no
Section titled “3. dbtCharts.com: no”Not for any user-tier source. Not for DuckDB either: DuckDB is service tier
permanently, but deny_tagged is enforced in the executor, and a hosted
chart would read around it.
The gate
Section titled “The gate”The model’s YAML is checked against an allow-list before dct sees it.
Anything not named is refused, so a field a later dct release adds is
refused until someone decides it is safe.
| The model may write | Rule |
|---|---|
| top level | charts and exactly one of rows, cols, grid |
| a chart | query, type, x, y, color, sort, style |
query |
the one name we supply; never SQL |
type |
bar, line, area, scatter, pie, donut, table, histogram, heatmap |
x, y, color, sort.by |
a column of the governed result |
style |
aspect_ratio, number_format, orientation, stack |
The service writes everything else: the title (the promoter’s, from the
catalog), the single query SELECT * FROM result, its file source, and a
dbt_charts.yml with no sources. dct runs with an empty environment, so
there is no profile or warehouse variable for it to find.
What the spike found
Section titled “What the spike found”Run against the local stack: the promoted candidate Net Revenue by Country for FY2026, through the gateway, as the personas in 05.
| Step | Result |
|---|---|
alice (Data.Analyst) runs the promoted query |
3 rows; the executor added its TOP 500 |
alice asks for dim_customer.email |
refused: Data.Analyst may not read dbo.dim_customer.email. No file, so no chart. |
carol (Data.Finance) asks for the same column |
400 rows. The difference is the executor’s decision, not the chart’s. |
| the model writes chart YAML from column names and types only | rendered on the second attempt, both times; dct validate named the mistake exactly (sort.direction, then sort on a table) |
dct renders with no credentials |
SVG and PNG, fully offline |
Seven hostile boards, each refused by the gate. What dct 0.8.0 does with
them on its own:
| Board | dct alone |
|---|---|
a values query with typed-in numbers |
renders them |
an http query |
makes the request (pointed at a closed local port) |
a chart with link: |
renders it, with the URL template embedded |
a free-text callout |
renders it |
| a column the caller cannot see | validate passes; render fails |
| a named warehouse, no credentials | validate passes; render fails |
| SQL inline in the chart | validate fails |
It does protect one thing on its own: DuckDB file access is disabled, so
SELECT … FROM read_text('/etc/hosts') over a file source was refused.
| Risk | Effect |
|---|---|
| Released days before the spike; 0.8.0 when it ran | the YAML and CLI may change under us; the allow-list is what keeps a new field from becoming a new route |
| Semantic Layer support is planned, not shipped | our metric names come from OpenMetadata, not dbt; nothing maps them yet |
ref() assumes a dbt project |
we have none; charts would read a file, which is what option 1 wants anyway |
| Another tool in the image | both executors (Python and Go) would need it, or a separate renderer would |
- Decide where rendering runs: inside each executor (Python and Go would
both need
dct), or a separate renderer that takes the governed rows. - Turn the gate and the hostile boards into a contract with cases, so it is checked in CI rather than once by hand.
- Keep option 2 closed until OpenMetadata has a service type for it.