Skip to content

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.

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.

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.

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:

vite.config.ts
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.

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.

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.

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:

Terminal window
curl -X POST -H "Authorization: Bearer $AURA_API_KEY" \
https://aura.strangeworks.com/api/v1/projects/$PROJECT_ID/run/dashboard/commands/build_artifact

That 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.

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.

The process serves the build it started with, so an edit to a running dashboard is invisible until it restarts:

Terminal window
curl -X POST -H "Authorization: Bearer $AURA_API_KEY" \
https://aura.strangeworks.com/api/v1/projects/$PROJECT_ID/run/dashboard/restart

Every 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.

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.

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.

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.

Terminal window
aura dashboard

prints 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.