Authoring HTML
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 type | Payload | Purpose |
|---|---|---|
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 listens | What event.origin is | What to allowlist |
|---|---|---|
Your shell (message in the iframe) | The Admin parent window that posts theme, style, and responses | Admin origins for your region (for example https://admin.tago.io), not the API host |
Admin host (message on the parent) | Your iframe document origin | The 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.