---
title: Dashboard isolation
description: A published dashboard is arbitrary author-written HTML, so it is served in a sandbox that cannot reach your Dashies session. What that costs, precisely.
updated: 2026-08-05
---

A Dashies dashboard is arbitrary HTML that somebody's AI wrote. It is *supposed*
to run scripts: that is how the charts draw. So the interesting question is not
whether script runs, but what it can reach.

## The problem being solved

Published dashboards are served from the same domain as the Dashies app. Without
a countermeasure, a script inside anyone's dashboard would share an origin with
the application, and could read a viewer's stored session token, their cookies,
and make authenticated requests as them. Opening a colleague's dashboard would be
enough.

## The countermeasure

Every dashboard is served with this response header:

```text
Content-Security-Policy: sandbox allow-scripts
```

That single directive puts the document in an **opaque origin**. It gets a fresh,
unique origin that matches nothing, including the application's. Scripts run,
because `allow-scripts` is opted in. Nothing else is.

Concretely, and these are properties the dashboard runtime itself is written
around:

- **No storage of any kind.** `localStorage`, `sessionStorage`, cookies, and
  IndexedDB are all unavailable. An opaque origin has no storage to belong to.
- **No same-origin authenticated request.** A request back to the Dashies
  application is cross-origin from an opaque origin and carries no session.
- **No new windows or tabs.** Opening an auxiliary browsing context is not
  granted, so a link with `target="_blank"` is silently swallowed. See the
  exception below.

Private dashboards additionally refuse to be framed at all, so an attacker's page
cannot wrap one and harvest what a tricked viewer clicks.

The one place Dashies frames a dashboard itself, the version preview inside the
app, keeps the sandbox exactly as it is and relaxes only *who may frame it*, to
the Dashies app and nothing else. A previewed dashboard is no less isolated from
the app than a served one.

## What it does not do

**It does not stop network calls.** This is worth being blunt about, because it
is the thing people assume and it is not true.

The header carries no `connect-src`, no `default-src`, and no `script-src`. A
dashboard can fetch from any origin it likes. Dashies relies on this: the
engine that powers large row-level dashboards is downloaded at view time from a
separate data domain, under exactly this header.

So the guarantee is **isolation from your Dashies account**, not confinement to
the page. A dashboard's script cannot become you. It can talk to the internet,
and the data in the page is data it can send.

The practical consequence: a published dashboard should be treated as code you
are running, on the same footing as any other HTML somebody handed you. If you
would not open an untrusted colleague's HTML file, do not open their dashboard
either. This is why arbitrary script inside a dashboard is not itself treated as
a vulnerability, while anything that lets that script reach the application, a
viewer's credentials, or another tenant's data very much is.

## The compatibility cost

Isolation is not free, and the cost lands on the author.

**Anything relying on browser storage breaks.** A dashboard that tries to
remember a viewer's preferred filter in `localStorage`, or set a cookie, will
throw rather than silently no-op. There is no way to opt out per dashboard.

This is why the runtime keeps active filters in the **URL hash** instead. A
filtered view is a shareable link that survives a reload, which turns out to be
the better design anyway, but it is a design forced by the sandbox rather than
chosen freely.

**Links that open a new tab do not open one.** If your dashboard links out to a
source system, use an ordinary same-tab link and test it, rather than assuming
`target="_blank"` behaves as it would on a normal page.

:::note{title="One asymmetry between plans"}
A dashboard published by a free-tier account carries a small "Powered by Dashies"
badge, and the badge is a link. So that the link actually works, the sandbox on
those responses is relaxed just enough to permit opening a new tab.

The side effect: **a `target="_blank"` link works on a free-plan dashboard and
stops working when the owner upgrades**, because the badge and its relaxation go
away together. If a link in your dashboard mysteriously stopped opening after an
upgrade, this is why. Switch it to a same-tab link.
:::

**Anything expecting a normal origin should be tested, not assumed.** The
document only receives capabilities the header opts into, and the header opts
into scripting. If you are reaching for a browser feature beyond drawing and
interaction, publish a small test dashboard and confirm it works before building
on it.

## Where the data lives

For most dashboards the numbers are inline in the file itself, so isolating the
document isolates the data with it.

When a dataset is too large to inline, it is served as a separate file from a
different domain. That split is deliberate: it keeps the dashboard document
cross-origin to the app, which is what preserves everything above.

A **private** dataset served this way is protected by a signed link with a
four-hour life, minted only while serving the dashboard to somebody already
authorized to see it. There is no endpoint that will sign a link on request, so
there is no way to get one without already having the access it grants. Any bad
or expired signature gets the same refusal, with nothing distinguishing which
kind of bad it was, and private responses are never stored in a shared cache
where they could be replayed.
