Malloy Documentation
search

A Publisher package can ship a public/ directory of plain web files (an index.html plus CSS, JavaScript, and images) right next to its .malloy files. Publisher serves that directory and gives the page a small runtime, Publisher.query(...), that runs Malloy queries against the package's models and hands back plain JSON rows. You render those rows with whatever you already use: Chart.js, D3, a hand-written table, or Malloy's own <malloy-render>. There is no build step, no npm install, and no framework to learn.

This is the most direct of Publisher's three embedding paths. The Publisher SDK is the React component path, and the REST API is the raw HTTP contract. Use an HTML data app when you want a self-contained dashboard with no toolchain; use the React SDK when you are building a larger application and want managed components and notebook reuse.

How a package becomes an app

my-package/
  publisher.json        # package manifest (name, version, description)
  carriers.malloy       # one or more models
  carriers.parquet      # the data the models read
  public/               # everything in here is web-served
    index.html
    app.js

Publisher serves the contents of public/ at:

/environments/<env>/packages/<pkg>/<file>

so public/index.html is the package's landing page. Only public/ is reachable over the web. The models, the data files, and publisher.json stay private and are reached only through the query API, which applies the same governance (source filters, access modifiers, authorize rules) as any other Publisher client. A package becomes a data app simply by having a public/ directory; there is no flag to set.

Try the example

The repo ships a complete example at examples/html-data-app: a carrier dashboard with filters, KPI tiles, two charts, and a data table, plus an iframe embed demo. Clone Publisher and serve the example with live reload:

git clone https://github.com/malloydata/publisher.git
cd publisher

# Point a small server config at the example package.
mkdir -p /tmp/publisher-demo && cp -R examples/html-data-app /tmp/publisher-demo/
cat > /tmp/publisher-demo/publisher.config.json <<'JSON'
{
  "frozenConfig": false,
  "environments": [
    {
      "name": "demo",
      "packages": [{ "name": "html-data-app", "location": "./html-data-app" }],
      "connections": []
    }
  ]
}
JSON

# Start the server in watch mode.
npx @malloy-publisher/server --server_root /tmp/publisher-demo --watch-env demo

Open http://localhost:4000/environments/demo/packages/html-data-app/. Edit a file under public/ and the page reloads on its own; edit carriers.malloy and the package recompiles. See examples/html-data-app/README.md for the full walkthrough.

The runtime

Load the runtime once per page with a root-relative script tag, so it resolves through Publisher whatever environment or package the page lives in:

<script src="/sdk/publisher.js"></script>

It adds a single global, window.Publisher, with no dependencies. The smallest page that talks to a model is:

<!doctype html>
<title>Carriers</title>
<pre id="out"></pre>
<script src="/sdk/publisher.js"></script>
<script>
  Publisher.query("carriers.malloy", "run: carriers -> by_letter").then((rows) => {
    document.getElementById("out").textContent = JSON.stringify(rows, null, 2);
  });
</script>

Running queries

Publisher.query(modelPath, malloy) runs a Malloy query against one model in the current package and resolves to an array of plain row objects. modelPath is the model file's path within the package, with / separators. The second argument is any Malloy query string.

// Run a named view defined in the model.
const rows = await Publisher.query("carriers.malloy", "run: carriers -> by_letter");
// rows -> [{ letter: "A", n: 23 }, { letter: "B", n: 17 }, ...]

// Refine a view with a filter at call time.
await Publisher.query("carriers.malloy", "run: carriers -> by_letter + { where: letter = 'A' }");

// A single-row KPI view: read element [0].
const [kpis] = await Publisher.query("carriers.malloy", "run: carriers -> kpis");
document.getElementById("total").textContent = kpis.total;

Define frontend-friendly views in the model, one per tile, pre-aggregated and pre-sorted. That keeps the page's query strings short and the work on the server. A dashboard usually fires several at once and renders them together:

const [byLetter, byBucket, kpisRows] = await Promise.all([
  Publisher.query("carriers.malloy", "run: carriers -> by_letter"),
  Publisher.query("carriers.malloy", "run: carriers -> by_size_bucket"),
  Publisher.query("carriers.malloy", "run: carriers -> kpis"),
]);

From there it is ordinary front-end code. With Chart.js, for example:

const rows = await Publisher.query("carriers.malloy", "run: carriers -> by_letter");
new Chart(document.getElementById("byLetter"), {
  type: "bar",
  data: {
    labels: rows.map((r) => r.letter),
    datasets: [{ label: "Carriers", data: rows.map((r) => r.n) }],
  },
});

If a query fails, the promise rejects with an Error whose message starts with Publisher.query:. The error carries error.status (the HTTP status) and error.response (the parsed body), so you can tell a missing required filter from a compile error or a permission failure.

If you would rather let Malloy draw the chart, call Publisher.queryFull(modelPath, malloy). It takes the same arguments but resolves to the full Malloy result envelope, which you pass straight to a <malloy-render> element.

Both query and queryFull take an optional third argument for runtime options: filterParams (values for the model's declared parameters, its givens), sourceName and queryName, bypassFilters, and environment / package overrides. When a filter value comes from untrusted input, declare a parameter on the model and pass it through filterParams rather than building the where: string by hand, so the server formats it safely. The in-repo authoring reference lists every option.

Embedding in another page

A page can be embedded in another page as an auto-resizing iframe with Publisher.embed:

<script src="https://your-publisher/sdk/publisher.js"></script>
<div id="dashboard"></div>
<script>
  const handle = Publisher.embed("#dashboard", {
    src: "https://your-publisher/environments/demo/packages/html-data-app/index.html",
  });
  // handle.destroy() removes the iframe and its listeners.
</script>

embed(selector, options) mounts an iframe into the matched element and returns { iframe, destroy() }. The iframe is sandboxed. When you omit height, the embedded page measures its own content and tells the host how tall to make the frame, and the host only trusts those messages from the iframe it created. You write none of that wiring; pass a fixed height to opt out of auto-sizing, or allow to set the iframe's permissions policy.

For a same-origin embed, the browser's cookies authenticate the iframe and you pass nothing extra. For a cross-origin embed (your customer's app on another domain), mint a signed token on your server and pass it as options.token; the embedded page receives it and authenticates with it. The token exchange is covered in the Publisher SDK guide.

Live reload while you build

Start the server with --watch-env <env> (or set PUBLISHER_WATCH=<env>) and Publisher mounts that environment's local packages in place and watches them. Editing a .malloy file recompiles the package; editing a file under public/ refreshes any open page. The runtime subscribes to a server-sent-events stream and reloads automatically, so you keep the browser open and just edit. Leave watch mode off in production, where it reports as disabled and no reloads fire.

A note on auth and secrets

By default the runtime sends the browser's cookies with every request, so a page served to a signed-in user queries as that user with no extra code. To use a bearer token instead, call Publisher.setToken(token) before querying, and Publisher.setToken(null) to go back to cookies.

Everything you put under public/ is web-served as-is, so keep secrets out of it. Your protection lives in the models and the database, behind the query API, where source filters, access modifiers, and authorize rules decide what each caller may see.

Next steps

  • Example app source for a complete package with filters, charts, KPIs, and an embed demo.

  • The in-repo authoring reference, docs/html-data-apps.md, for the full Publisher.query / Publisher.embed API, the page and event contracts, and the security model.

  • REST API for the HTTP contract the runtime calls.

  • Publisher SDK for the React component path and the signed-token exchange.