Skip to main content

Authoring HTML

warning

Experimental. This feature may change or be removed.

Your shell is a normal HTML document loaded in an Admin iframe. The host talks to it with window.postMessage. There is no custom-widget library and no requirement to host the page yourself.

To list and run saved TagoSQL queries, use the request/response bridge in SQL Bridge. This page covers ready, theme, and style only.

Contract

1. Shell signals ready

When your page has registered message listeners and is ready:

parent.postMessage({ type: "dashboard:ready" }, "*");

Post ready after you attach the message listener so the first host reply is not dropped.

2. Host sends theme and optional style

The host listens for dashboard:ready from your iframe, then posts:

Message typePayloadPurpose
dashboard:theme{ theme }"dark" or "light" to match Admin.
dashboard:style{ style }Optional host-driven style object. May be empty.

Theme may be sent again when the Admin color scheme changes, without a full shell reload.

3. Data over request/response

The shell initiates data loads. Post dashboard:request for sql.list and sql.run; the host answers with dashboard:response (same id). Full shapes, errors, and examples: SQL Bridge.

Minimal example

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Custom Dashboard</title>
<style>
:root {
color-scheme: light dark;
}
body {
font-family: system-ui, sans-serif;
margin: 1.5rem;
}
</style>
</head>
<body>
<h1>Custom Dashboard</h1>
<p id="theme">theme: ...</p>
<script>
const themeEl = document.getElementById("theme");

window.addEventListener("message", (event) => {
// Prefer checking event.origin against the Admin origin in production
// (the parent that posts theme/style), not the API host of this HTML.
const msg = event.data;
if (!msg || typeof msg !== "object") return;

if (msg.type === "dashboard:theme") {
document.documentElement.dataset.theme = msg.theme; // "dark" | "light"
themeEl.textContent = "theme: " + msg.theme;
}

if (msg.type === "dashboard:style") {
Object.assign(document.body.style, msg.style ?? {});
}
});

// Signal ready so the host can send theme (and optional style)
parent.postMessage({ type: "dashboard:ready" }, "*");
</script>
</body>
</html>

This matches the snippet shown on the empty state in Admin. For fetching data, add the request helper from SQL Bridge.

Origin checks

There are two directions. Do not mix them up:

Who listensWhat event.origin isWhat to allowlist
Your shell (message in the iframe)The Admin parent window that posts theme, style, and responsesAdmin origins for your region (for example https://admin.tago.io), not the API host
Admin host (message on the parent)Your iframe document originThe API host that serves GET /dashboard/{id}/html

Admin posts into the iframe with the API origin as the postMessage target so the browser only delivers to that document. The sender origin on those messages is still Admin. If your shell allowlists only the API host, every legitimate dashboard:theme, dashboard:style, and dashboard:response message is dropped.

Ignore messages from origins you do not expect.

What not to do

  • Do not expect the custom-widget SDK handshake or realtime variable streams. That is a different product surface; see Custom Dashboard vs Custom Widget.
  • Do not embed profile tokens in the HTML file. Data comes through the postMessage request/response bridge you initiate; Admin uses the signed-in session on the host side.
  • Do not rely on an in-app editor: download, edit, upload (HTML shell).

Sandbox

Admin loads the shell in an iframe with a restricted sandbox (allow-scripts and allow-same-origin in the current host). Design for that environment: no parent DOM access beyond postMessage, and careful use of storage and navigation APIs.