Bring your own dashboard
A project’s dashboard is a real web app running in the project’s own sandbox, reading the latest
solution off the workspace. Aura ships one and its agent adapts it, but you can ship your own instead:
put a web project at dashboard/ in your model folder and it deploys to run/dashboard/ with the
solver. Aura serves whatever is there.
Any framework, any dependency from the public registry, server-side code if you need it. Nothing in it is Aura’s — no component library, no design system.
The contract
Section titled “The contract”Five things, and no more:
| Requirement | Why |
|---|---|
Something serves HTTP on $PORT, bound to $HOST (0.0.0.0) |
that is how the proxy reaches it |
Asset URLs are relative (Vite: base: "./") |
the page is served under a per-viewer prefix that changes per viewer |
Read data from $AURA_PROJECT_DIR |
the workspace is the source of truth: solutions/out.json, run/artifacts/inputs.json |
Commit pnpm-lock.yaml |
installs are frozen, so a deploy is reproducible — see your package manager, and ours |
Keep a release cooldown in pnpm-workspace.yaml |
whatever you build ships to viewers’ browsers |
Not a requirement, but worth doing: write build output to $AURA_DASHBOARD_BUILD_DIR. Ignoring it costs
build time, not correctness — see build output directory.
The first start installs dependencies and can take a couple of minutes; viewers see a self-refreshing
holding page until the process is up. Adding a dependency to package.json re-resolves the lockfile on
the next start.
How Aura decides what to run
Section titled “How Aura decides what to run”Two commands: a build, and a start. Aura resolves each from your project, in this order.
| Aura runs | |
|---|---|
| build | what aura.json says → your build script, if you declare one → the Vite build, if the project uses Vite → nothing |
| start | what aura.json says → your start script, if you declare one → vite preview, if the project uses Vite → an error naming what it looked for |
Declaring a start script is the reliable answer. Declare one and it is what runs, which also
makes it the thing that has to honour $PORT and $HOST. Aura only guesses when you have not said.
The one framework Aura guesses for is Vite — a plain Vite app has no start script of its own,
which is the case this resolution exists for, and vite preview serves the build without assuming
where the build put anything. That covers frameworks built on Vite too, SvelteKit among them.
For anything else — Next.js, Nuxt, Astro, a hand-written server — declare a start script, or name
the command in aura.json. Aura deliberately carries no preset for them: a preset is a guess about a
framework’s output layout, and a wrong guess fails when a viewer first opens the dashboard rather than
when you deploy. Two lines of config beat a guess, and the error page tells you exactly that.
Build output directory
Section titled “Build output directory”Aura sets $AURA_DASHBOARD_BUILD_DIR to a fast local directory for build output. Point your build
there — it is significantly faster than writing to the workspace. For Vite:
export default defineConfig({ build: { outDir: process.env.AURA_DASHBOARD_BUILD_DIR || "dist", },});The fallback to dist/ keeps the project working outside Aura. Your start command serves from the
same place.
The directory is ephemeral: it lives on the container, not in the workspace, and a sandbox sleeps after ten minutes idle. Every cold start finds it empty, so your start command must build when there is nothing there (and on a slow first build, listen first and hold the page rather than build first — a start that has not opened its port within a few minutes is killed). If you rely on Aura’s Vite guess, that is handled for you: the guessed build and preview commands both point at this directory. Aura’s own template does the same in its server.
aura.json
Section titled “aura.json”Drop one in your dashboard directory. It wins over both your scripts and the Vite guess:
{ "build": "npm run build", "start": "node ./server.js"}Both keys are optional, and each is resolved on its own — omit build and Aura falls back through the
table above. "build": null is different from omitting it: it says there is nothing to build, which is
what you want when your start command builds for itself.
Your package manager, and ours
Section titled “Your package manager, and ours”Aura installs your dependencies with pnpm, and that part is not configurable: it keeps the dependency tree on container disk rather than the workspace mount, which is the difference between an install that takes seconds and one that takes minutes and can corrupt itself halfway.
That constrains one thing only — commit a pnpm-lock.yaml. If you develop with npm or yarn, run
pnpm import once to convert your existing lockfile, then commit the result. Without it Aura resolves
your dependencies fresh on the first start, which works but is not the build you tested.
Nothing else has to be pnpm. pnpm build and pnpm start above are just how Aura runs your
package.json scripts, and a command you write in aura.json needs nothing from pnpm at all —
node ./server.js is a perfectly good start command.
Running your own commands
Section titled “Running your own commands”Beside build and start — which Aura drives, and whose meaning it depends on — aura.json can
declare named commands of your own, each listing the events it wants to run on:
{ "commands": { "build_artifact": { "run": "pnpm run build_artifact", "on": ["snapshot"] } }}Aura runs them; it has no idea what any of them do. The events are:
| Event | Fired when |
|---|---|
build |
your dashboard has started and is serving — after the port is open, so a slow command never delays a viewer |
solve |
a solve finished and its results are on disk |
snapshot |
a snapshot is being captured — before the workspace is frozen, so whatever your command writes is inside it. Fired for both kinds: the one you take yourself, and the one Aura takes automatically when a solve completes |
checkout |
a past run or a deployment has been checked out over the workspace |
Or run one yourself, whenever you like:
curl -X POST -H "Authorization: Bearer $AURA_API_KEY" \ https://aura.strangeworks.com/api/v1/projects/$PROJECT_ID/run/dashboard/commands/build_artifactThat answers 200 with {"ok": …, "ran": [...], "detail": …}, or 404 if your project declares no
command by that name. A command that fails never fails the thing that triggered it — a solve, a
capture and a checkout all carry on regardless — so check ok if you care.
Subscribe to what you actually need. Aura’s own dashboard subscribes to snapshot alone: it is the
only event whose output anything reads back, and every capture fires it.
What our own dashboard uses this for
Section titled “What our own dashboard uses this for”The dashboard Aura ships declares exactly one command, and it is worth copying: an export that
renders the current solution as one plain HTML file at run/artifacts/dashboard.html — metric cards
and tables as markup, styles and fonts inlined, no script. Because run/ is included in snapshots,
every capture then carries a readable copy of that run, which anyone can fetch through the Files API
and open with no sandbox at all. A past run stays readable long after its container is gone.
Nothing about that is built into Aura. If you want the same thing, declare a command that produces one self-contained file: inline your CSS and fonts, and render the data your page would otherwise fetch, since a file on disk has no server to answer it. If your dashboard renders on the server or computes per request, declare no such command — you lose the static copy and nothing else.
Restarting it
Section titled “Restarting it”The process serves the build it started with, so an edit to a running dashboard is invisible until it restarts:
curl -X POST -H "Authorization: Bearer $AURA_API_KEY" \ https://aura.strangeworks.com/api/v1/projects/$PROJECT_ID/run/dashboard/restartEvery call restarts — there is no “already running” case to check for. It returns as soon as the old
process is gone rather than waiting for the new one, so the next page view lands on the holding page
and refreshes itself. Use it after changing aura.json, after adding a dependency, after editing the
app, or to recover a dashboard that has stopped responding.
Restarting is also how a change reaches somebody already looking at the dashboard. Aura’s Run tab reloads its frame when the process is replaced, so you do not have to ask them to refresh — and because that is handled by whatever embeds the page, your app does not have to implement it.
Do not size or scroll the page yourself
Section titled “Do not size or scroll the page yourself”This is the one that catches people out.
Render at your content’s natural height. Do not set a height on html or body, do not give the
page its own scroll container, and do not assume a viewport. How the dashboard is presented belongs
to whoever embeds it: Aura’s Run tab grows the frame to fit the whole page so nothing is cut off and the
workspace scrolls as one document, while another host may pin a height and let the frame scroll. Both
work — but only if the page has not already decided for them.
Aura measures the page and reports its height to the embedder for you; you do not have to send anything.
Getting a copy out
Section titled “Getting a copy out”The Run tab’s Export prints the dashboard — the browser’s own dialog, so “Save as PDF” is there
too. You implement nothing: Aura calls print() from inside the frame, which an embedder cannot do
across origins, and adds print-color-adjust: exact so a dark dashboard does not print as invisible
ink. Printing takes the whole document, not the part currently on screen.
Reacting to a new solve
Section titled “Reacting to a new solve”The page can subscribe to the project’s WebSocket with the view ticket in its own URL and re-read when a solve finishes:
const [, projectId, ticket] = /\/projects\/([^/]+)\/run\/dashboard\/d\/([^/]+)\//.exec(location.pathname) ?? [];const socket = new WebSocket( `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}` + `/api/v1/projects/${projectId}/ws?ticket=${ticket}`,);socket.onmessage = (event) => { const { type } = JSON.parse(event.data); if (type === "solve_complete" || type === "files_changed") refresh();};The ticket is scoped to this one dashboard and only receives those two events. Fall back to a slow poll when the socket cannot be opened — a page opened outside Aura has no ticket to use.
Opening it
Section titled “Opening it”aura dashboardprints the URL and opens it — for the selected project, or another one with --project <id>. The link carries a short-lived view ticket rather than your API key, so it
is safe to paste or drop in an <iframe src>. See Dashboard.