A local dashboard for Dapr developers that offers a live view of everything Dapr running on your machine, plus guided builders for authoring Dapr components and resiliency policies.
See the codeA local dashboard for Dapr developers that offers a live view of everything Dapr running on your machine, plus guided builders for authoring Dapr component and resiliency YAML.


The Diagrid Dapr Dev Dashboard is a companion for local Dapr development. It inspects the
apps you start with dapr run / dapr run -f, Aspire, Docker Compose, or Dapr
Testcontainers (e.g. Spring Boot apps run with mvn spring-boot:test-run and
dapr-spring-boot-starter-test), and surfaces everything about them — sidecars, workflows,
actors, subscriptions, components, resiliency policies, configurations, and logs.
It also helps you author Dapr resources. The Component Builder walks you through picking a component type from the full Dapr catalog, filling in its metadata fields, and choosing an authentication profile; the Resiliency Builder composes resiliency policies (timeouts, retries, circuit breakers) and applies them to targets (apps, actors, components). Both wizards end in a YAML preview you can copy or download into your project.
The dashboard ships as a standalone binary published on GitHub Releases.
Install (one-liner):
macOS / Linux — installs to ~/.local/bin
curl -sSL https://raw.githubusercontent.com/diagridio/dev-dashboard/main/scripts/install.sh | sh
Windows (PowerShell) — installs to %LOCALAPPDATA%\Programs\diagrid-dev-dashboard
iwr -useb https://raw.githubusercontent.com/diagridio/dev-dashboard/main/scripts/install.ps1 | iex
To pin a specific version, set VERSION before piping:
macOS / Linux
curl -sSL https://raw.githubusercontent.com/diagridio/dev-dashboard/main/scripts/install.sh | VERSION=vX.Y.Z sh
Windows
$env:VERSION='vX.Y.Z'; iwr -useb https://raw.githubusercontent.com/diagridio/dev-dashboard/main/scripts/install.ps1 | iex
If the install directory is not on your PATH, the script prints the export line to add.
Install with Go (≥ 1.26):
go install github.com/diagridio/dev-dashboard@latest
Note:
go installnames the binary after the module path, so it producesdev-dashboard, notdiagrid-dev-dashboard— rename it afterwards (mv "$(go env GOPATH)/bin/dev-dashboard" "$(go env GOPATH)/bin/diagrid-dev-dashboard") or use the install script above.
Manual download:
Download the archive for your platform from the GitHub Releases page, extract it, and place diagrid-dev-dashboard (or diagrid-dev-dashboard.exe) on your PATH. Verify with:
diagrid-dev-dashboard --version
# Start on the default port (9090) and open your browser automatically
diagrid-dev-dashboard
# Start on a custom port
diagrid-dev-dashboard --port 8080
# Enable diagnostic logging to stderr (server startup, app discovery, state-store connection, log streams, workflow operations)
diagrid-dev-dashboard --verbose
No additional setup is needed: the dashboard discovers running Dapr apps the same way
dapr list does, so anything started with dapr run / dapr run -f, Aspire, Docker
Compose, or Dapr Testcontainers shows up within one refresh cycle. Testcontainers apps
(e.g. a Spring Boot app under mvn spring-boot:test-run) need zero configuration: the
dashboard finds the Testcontainers-managed daprd container, pairs it with your host app
process, and even reads workflows from an in-memory state store via the sidecar itself.
Tip: once the dashboard is open, try the Konami code (↑ ↑ ↓ ↓ ← → ← → B A) and have some fun! ;)
With --mode/DEVDASHBOARD_MODE unset (the default for host use), the dashboard performs the
complete scan across all discovery sources described above. Setting a mode restricts every
dashboard surface — applications, workflows, state stores, the Control Plane view, and log
targets — to a single source; filters are exclusive and never combined:
--mode dapr-run — host dapr run processes only (Control Plane shows the dapr init containers).--mode compose — Docker Compose containers only (Control Plane shows compose-run placement/scheduler).--mode test-containers — Testcontainers discovery only (no control-plane detection yet).--mode aspire — Aspire resources only. Inside an AppHost-managed container (the
DEVDASHBOARD_APP_* contract is present) this is the container serving posture described
below; on a plain host it filters the process scan to Aspire-managed apps.compose and test-containers require a container runtime (docker or podman) and fail at
startup without one. --bind (default 127.0.0.1, 0.0.0.0 in aspire container posture)
controls the listen address alongside --port.
The dashboard also ships as a container image, purpose-built for embedding inside a
.NET Aspire AppHost via the
diagrid-labs/dashboard-aspire hosting
integration (which will be rewritten against the contract below). It also runs standalone
with a hand-written docker run.
Image: ghcr.io/diagridio/dev-dashboard, tagged :X.Y.Z (in lockstep with binary
releases) and :latest. Prerelease tags (e.g. v1.5.0-rc.1) publish their own version tag
but do not move :latest. The image bakes in DEVDASHBOARD_MODE=aspire and serves on port
8080 bound to 0.0.0.0.
In aspire mode, discovery is restricted to the env contract below (no host process scan, no Docker Compose scan), and the following are disabled: app lifecycle controls (start/stop/restart), the control-plane page, log tailing, self-update/update-check, and automatic browser opening.
Try it:
docker run --rm -p 8080:8080 \
-e DEVDASHBOARD_APP_COUNT=1 \
-e DEVDASHBOARD_APP_0_ID=myapp \
-e DEVDASHBOARD_APP_0_DAPR_HTTP=http://host.docker.internal:3500 \
ghcr.io/diagridio/dev-dashboard:latest
Mode switch:
| Source | Values | Default |
|---|---|---|
--mode flag / DEVDASHBOARD_MODE env | dapr-run, compose, test-containers, aspire | unset (complete scan) |
App discovery — one set of _<i>_* vars per app, for i = 0..DEVDASHBOARD_APP_COUNT-1:
| Env var | Required | Meaning |
|---|---|---|
DEVDASHBOARD_APP_COUNT | yes | number of apps (0 is valid: empty dashboard) |
DEVDASHBOARD_APP_<i>_ID | yes | Dapr app-id |
DEVDASHBOARD_APP_<i>_DAPR_HTTP | yes | daprd HTTP base URL, reachable from the dashboard container (e.g. http://myapp-dapr:3500) |
DEVDASHBOARD_APP_<i>_NAMESPACE | no | per-app Dapr namespace; defaults to DEVDASHBOARD_NAMESPACE. Used for app-scoped workflow operations — fetching one instance's history, an app-filtered workflow list/stats, and force delete — so those honor the app's own namespace. The store-wide workflow list, stats, and app-id dropdown (all-apps scans) still use the global DEVDASHBOARD_NAMESPACE |
DEVDASHBOARD_APP_<i>_LABEL | no | display name; defaults to the app-id. Shown in the applications list and app detail header whenever it differs from the app-id |
Validation is fail-fast at startup: a missing or non-numeric DEVDASHBOARD_APP_COUNT, any
missing required per-app var, or an unparsable DAPR_HTTP URL exits with an error naming the
exact variable.
Security note: in aspire mode the dashboard drops the loopback Host check (the container
is addressed by its Docker-network name or a published port). Without DEVDASHBOARD_ALLOWED_HOSTS
it accepts any Host, which means a malicious web page using DNS rebinding can reach the API even
when the port is published only to localhost. Setting DEVDASHBOARD_ALLOWED_HOSTS to the
hostnames the dashboard is served under closes that hole (loopback names are always allowed).
Mutating requests are still protected by a normalized same-origin check. The dashboard is a local
development tool — never expose it publicly.
Serving and features:
| Env var / flag | Default (aspire) | Meaning |
|---|---|---|
DEVDASHBOARD_PORT / --port | 8080 | listen port |
DEVDASHBOARD_BIND / --bind | 0.0.0.0 | bind address |
DEVDASHBOARD_STATESTORE_FILE / --statestore | unset | path to a mounted Dapr state-store component YAML; enables the Workflows page |
DEVDASHBOARD_NAMESPACE / --namespace | default | default Dapr namespace for workflow actor keys |
DEVDASHBOARD_RESOURCES_PATH | dir of DEVDASHBOARD_STATESTORE_FILE | extra component directories for the Resources page, os.PathListSeparator-separated |
DEVDASHBOARD_ALLOWED_HOSTS | unset (any host) | optional, aspire mode only; comma-separated hostnames the Host header is restricted to (loopback always allowed). Empty means any host. Set it to close the DNS-rebinding hole described above |
DEVDASHBOARD_MODE | aspire (baked into the image) | see mode switch above |
Precedence everywhere is flag > env > posture default.
On startup the dashboard checks GitHub for a newer release. If one exists, it prints a notice as the first line of output and the web UI shows an Update available indicator next to the version number in the Resources panel. The check is best-effort: it is skipped for source/dev builds and fails silently when offline.
Update to the latest release (no-op if already current)
diagrid-dev-dashboard update
Install a specific version (can downgrade or reinstall)
diagrid-dev-dashboard update 1.2.0
update downloads the release archive for your platform, verifies its SHA256
against the release checksums.txt, and atomically replaces the running binary.
Restart any running dashboard to use the new version.
If the dashboard does not behave as expected, run it with --verbose to print diagnostic logs to stderr:
diagrid-dev-dashboard --verbose
Logs are grouped by component= (values: server, discovery, workflow, registry, reconciler) and use levels INFO (normal milestones), WARN (degraded but still working, e.g. a state store that failed to initialise), and ERROR (an operation failed, e.g. the server could not bind its port). Without --verbose, no diagnostic logs are emitted.
Developers use the dashboard to observe and debug Dapr apps while building locally:
<app-id>||<key> — the same key shape
Dapr writes — and refuses a key that already exists unless you opt into overwriting; records can
be deleted individually or by multi-select. There is no in-place edit: overwrite the key instead.
Two limitations worth knowing:
workflow history and actor state are hidden by default (they live in the same keyspace
as your app's records — use Show internal keys to reveal them), and there is no
"last modified" column — Dapr's state components do not expose a modification timestamp,
so none can be shown. The Version column is the backend's etag: it changes on every write,
but it is not a time. Stores that cannot be opened directly — an in-memory store inside a
Testcontainers app, for example — cannot be browsed at all: Dapr's state API has no way to
enumerate keys, so there is nothing to page over.~/.dapr/dev-dashboard/connections.yaml (mode 0600). When more than one store is known, a
selector on the Workflows page lets you switch which one you browse.The UI is built for fast scanning and debugging: deep-linkable views, a global autorefresh control that doubles as a backend-connection indicator (data polling pauses while the backend is unreachable and resumes on recovery), full keyboard operability, and cross-navigation between related entities (app → component → "loaded by" app, etc.).
The dashboard is a local development tool, and only that:
dapr list does — from the local process table and local container runtime —
which has no meaning in-cluster.Run it on your own machine, alongside the apps you start with dapr run, Aspire, Docker
Compose, or Dapr Testcontainers.
Prerequisites: Go ≥ 1.26 and Node.js 20 (with npm). The binary embeds the React SPA via
go:embed, so the web assets (web/dist) must be built before the Go binary — make build
does both in the right order.
macOS / Linux:
make build # builds web/dist, then the Go binary at bin/diagrid-dev-dashboard
./bin/diagrid-dev-dashboard
Equivalent manual steps (if you don't have make):
cd web && npm install && npm run build && cd ..
go build -o bin/diagrid-dev-dashboard .
./bin/diagrid-dev-dashboard
Windows (PowerShell): make is usually unavailable, so run the steps directly:
cd web; npm install; npm run build; cd ..
go build -o bin/diagrid-dev-dashboard.exe .
.\bin\diagrid-dev-dashboard.exe
To build for a sub-path mount, set DASH_BASE_PATH before building (see
Mounting under a sub-path). On Windows that is
$env:DASH_BASE_PATH='/dashboard/' before the npm run build step.
Other useful targets: make test (Go unit + web suites), make test-go, make test-web,
make test-integration, make test-e2e, make tidy.
The dashboard is a passive observer: it discovers your app the same way dapr list does
and reads workflow data directly from your Dapr state store. You don't point it at your app —
you just run both on the same machine.
Prerequisites:
dapr init has been run. This creates ~/.dapr/components/statestore.yaml (a Redis store with
actorStateStore: "true") and starts Redis. That actorStateStore store is what Dapr
Workflows persist to, and what the dashboard reads.Steps:
dapr run --app-id order-processor --app-port 6001 -- <your app start command>
# or, for a multi-app project:
dapr run -f .
./bin/diagrid-dev-dashboard # opens http://localhost:9090
If the Workflows page is empty: the dashboard auto-detects state-store components from
~/.dapr/components and from the live --resources-path of running apps, then uses the one
marked actorStateStore: "true" (falling back to the first detected). Check:
./bin/diagrid-dev-dashboard --statestore ~/.dapr/components/statestore.yaml. You can also add a
store by hand via the connection manager on the Components page.default. For another namespace, pass
--namespace <ns>.state.in-memory, or a Testcontainers app whose store lives
inside the container) are served via their sidecar's gRPC workflow API instead — this
requires Dapr ≥ 1.17 and only works while the sidecar is running.Testcontainers apps (e.g. the Java quickstarts run with mvn spring-boot:test-run) need
none of the store setup above: workflows, components, and app details all come from the
Testcontainers-managed sidecar itself.
There are four suites: Go unit tests, Go integration tests, the web
(frontend) tests, and an opt-in Go e2e suite. The unit and web suites are self-contained.
The integration suite runs its state-store and workflow tests against a temporary SQLite
database plus real Redis, PostgreSQL, and MongoDB containers (via testcontainers-go), so it
needs Docker or Podman for full coverage; without one, the container-backed tests skip. The
e2e suite drives a real daprd and is local-only — it skips automatically when Dapr is not
installed (see below).
Prerequisites: Go ≥ 1.26 (Go tests) and Node.js 20 with npm (web tests).
Run everything (macOS / Linux):
make test # Go unit tests (with -race) + web tests
make test runs make test-go then make test-web. It does not run the Go integration
tests — run those separately (see below).
Go unit tests — gated by //go:build unit:
make test-go # = go test -tags unit -race ./...
# or directly:
go test -tags unit ./...
go test -tags unit -race ./cmd/... # one package, with the race detector
(make test-go uses gotestsum for nicer output if it's installed, otherwise plain go test.)
Go integration tests — gated by //go:build integration; they exercise the state-store and
workflow read paths (including a parity test of the workflow list/stats/detail against all four
supported backends), the parsed sidecar /v1.0/metadata, and the full assembled HTTP server.
Backends run as containers through testcontainers-go (skipped when no container runtime is
available) alongside a temp SQLite DB. They run in CI but are not part of make test:
make test-integration # = go test -tags integration -race ./...
# or directly:
go test -tags integration ./...
Some integration tests use golden files (testdata/golden/*); regenerate them after an
intentional shape change with -update, e.g.
go test -tags integration ./pkg/workflow -run Golden -update.
Go e2e tests — gated by //go:build e2e; they run a real Dapr workflow app under
dapr run and read its state back through the dashboard's own packages, validating against
state authored by a live runtime. They require a local Dapr install (dapr init) — dapr on
your PATH and daprd on PATH or in ~/.dapr/bin — and skip automatically when Dapr is
not found. They are local-only and not run in CI:
make test-e2e # = go test -tags e2e ./...
Web tests — Vitest:
make test-web # = cd web && npm install && npm test (vitest run)
# or from web/:
cd web
npm install
npm test # single run
npm run test:watch # watch mode
Windows (PowerShell) — make is usually unavailable, so run the commands directly:
go test -tags unit -race ./...
go test -tags integration ./...
cd web; npm install; npm test; cd ..
Tip: the Go tests are build-tag-gated, so a plain
go test ./...(without-tags unit,-tags integration, or-tags e2e) reports "no test files" for most packages. Always pass the tag.
make lint # lint-go (gofmt + go vet) + lint-web (eslint)
make lint-go # = gofmt check + go vet -tags unit ./...
make lint-web # = cd web && npm install && npm run lint (eslint .)
The Go checks (gofmt, go vet) and the web eslint run in CI on every push and pull request.
Pre-commit hook (optional): install a hook that lints only your staged files before each commit:
make hooks # symlinks .git/hooks/pre-commit -> scripts/pre-commit
It runs gofmt/go vet on staged Go files and eslint on staged web/ files. Bypass it for a
single commit with git commit --no-verify.
For maintainers with push access. Releases are built and published by the
releaseGitHub Actions workflow: pushing avX.Y.Ztag runs GoReleaser, which compiles the cross-platform archives pluschecksums.txtand publishes them to a GitHub Release. The version (diagrid-dev-dashboard --version) is injected from the tag via build-time ldflags. The same tag-driven run also builds and pushes the multi-arch container image toghcr.io/diagridio/dev-dashboardvia goreleaser. Prerelease tags (any-suffix, e.g.v1.5.0-rc.1) push their version-tagged image but skip the:latestmanifest, so release candidates never reach:latestconsumers.
Because go install cannot run npm, the release tag commit must embed the prebuilt
web/dist. scripts/release.sh handles this: it builds the SPA, creates a detached commit
that force-adds web/dist (past .gitignore), tags it, and returns you to your branch — so the
tagged commit ships the full UI for go install while main stays free of built assets.
Cut a release (macOS / Linux, or Git Bash / WSL on Windows — release.sh is a POSIX sh script):
main with a clean working tree.scripts/release.sh vX.Y.Z
It builds the SPA, creates the tag on a detached commit embedding the UI, and prints the
push command.git push origin vX.Y.Z
release workflow to finish. It publishes a GitHub Release with one archive per
platform + checksums.txt. After that, the install one-liners and
go install github.com/diagridio/dev-dashboard@vX.Y.Z resolve to the new version.Validate locally before tagging (optional, requires GoReleaser v2):
make release-check # validate .goreleaser.yaml
make release-snapshot # build a local snapshot into dist/ without publishing
The release matrix is 5 archives: macOS and Linux (amd64 + arm64) and Windows (amd64). There is no native Windows/arm64 build — Windows on ARM uses the amd64 build via emulation.
The dashboard is a single Go binary that embeds a React SPA and talks to your local Dapr sidecars and state store.
For the full architecture — how discovery, state-store election, the reconciler, the HTTP layer, and the SPA fit together, plus a guide to extending each part — see ARCHITECTURE.md. The summary below is the quick tour.
┌───────────────────────────────────────────────────────────────┐
│ diagrid-dev-dashboard (single Go binary) │
│ │
│ cmd/ cobra root, flags, serve boot, │
│ connection registry + reconciler │
│ pkg/server chi router + go:embed SPA │
│ pkg/discovery standalone.List() + /v1.0/metadata │
│ pkg/workflow list / history / purge │
│ pkg/state state record listing / add / delete │
│ pkg/statestore client (redis / postgres / sqlite / mongodb) │
│ pkg/controlplane docker/podman inspect + lifecycle │
│ pkg/metadata component metadata catalog │
│ pkg/resources component + configuration YAML loader │
│ pkg/logs file tail → SSE │
│ web/ React + Vite SPA → dist/ (embedded) │
└───────────────────────────────────────────────────────────────┘
│ HTTP /v1.0/* │ files / TCP │ docker/podman
▼ ▼ ▼
running daprd ~/.dapr, resource paths, control-plane
sidecars state store backend containers
Components
chi) — exposes a REST + JSON API (with SSE for log/stream tails) and
serves the embedded SPA. Each domain lives in an isolated pkg/* package (a service.go
plus its response types); the HTTP layer lives in pkg/server, one file per domain. No
domain package depends on cmd/.go:embed. Uses TanStack Query for polling/caching,
headless accessible primitives styled in-house, and a custom lightweight read-only YAML
highlighter (not Monaco). Client-side History-API routing (react-router-dom); the Go
server falls back to index.html for unknown paths and is base-path-aware. List
virtualization is planned, not yet in v1. The UI styling conventions (design tokens,
page anatomy, component classes) are documented in
web/STYLEGUIDE.md.Key dependencies & data sources
github.com/dapr/cli/pkg/standalone (the same mechanism as
dapr list). standalone.List() reads the local process table and is the source of truth
for existence/ports/PIDs; the /v1.0/metadata call per sidecar is enrichment (runtime
version, components, actors, subscriptions, extended metadata) and degrades gracefully when
a sidecar is down. A /v1.0/healthz check per sidecar drives the health badge — computed
on demand during each /api/apps fetch (no separate background poller), so its refresh cadence
follows the UI's autorefresh interval. A second scanner discovers Dapr apps running under
docker compose by inspecting compose-labelled containers (app id and ports from the daprd
argv, logs streamed from the container runtime). A third scanner discovers Dapr
Testcontainers sessions (org.testcontainers-labelled daprd containers, e.g. from
dapr-spring-boot-starter-test): the randomly-published HTTP/gRPC ports are re-read every
poll, the host app process is paired via the app port (real PID, uptime, runtime), and the
component YAML declared in test config is extracted from the container so it appears on the
Components page. All sources are merged, so one failing never hides the others.state.in-memory included), or any app when no store is openable — workflows are read live
from the sidecar's gRPC workflow API (Dapr ≥ 1.17) instead, per app. Purge uses the
official Dapr workflow API when reachable, with direct state-store key deletion as an
explicit force fallback.~/.dapr/dev-dashboard/connections.yaml (mode 0600). The pkg/metadata catalog drives the
add/edit forms, the workflow backend connects to the selected store lazily (on demand), and
secretKeyRef metadata is resolved through local secret stores
(secretstores.local.file / secretstores.local.env).~/.dapr and live
--resources-path directories read from daprd args.~/.dapr/logs/* and the appLogPath/daprdLogPath reported in
extended metadata, then streamed to the SPA over SSE.docker, else
podman): dapr_scheduler / dapr_placement are the self-hosted containers the dashboard
can start/restart/stop (allowlisted to those names). Container logs stream over SSE via
docker logs -f.GET /api/news endpoint so the SPA only ever talks to its
own origin.Portability — all logic lives in pkg/* domain packages with no dependency on cmd/,
the server mounts as a chi sub-router, and the SPA is an embedded fs.FS, so the whole
thing can later be re-mounted under a diagrid dashboard subcommand.
For the full architecture and extension guide, see ARCHITECTURE.md; for the
original design rationale, see
docs/superpowers/specs/2026-06-25-dev-dashboard-design.md.
Contributions are welcome! See CONTRIBUTING.md for how to report issues,
set up a development environment, and submit pull requests. All commits must be signed off
per the Developer Certificate of Origin (git commit -s).
Copyright © Diagrid Inc. Licensed under the Apache License 2.0.
TypeScript
51.7%
Go
45.3%
CSS
2.3%
A local dashboard for Dapr developers that offers a live view of everything Dapr running on your machine, plus guided builders for authoring Dapr components and resiliency policies.
See the codeA local dashboard for Dapr developers that offers a live view of everything Dapr running on your machine, plus guided builders for authoring Dapr component and resiliency YAML.


The Diagrid Dapr Dev Dashboard is a companion for local Dapr development. It inspects the
apps you start with dapr run / dapr run -f, Aspire, Docker Compose, or Dapr
Testcontainers (e.g. Spring Boot apps run with mvn spring-boot:test-run and
dapr-spring-boot-starter-test), and surfaces everything about them — sidecars, workflows,
actors, subscriptions, components, resiliency policies, configurations, and logs.
It also helps you author Dapr resources. The Component Builder walks you through picking a component type from the full Dapr catalog, filling in its metadata fields, and choosing an authentication profile; the Resiliency Builder composes resiliency policies (timeouts, retries, circuit breakers) and applies them to targets (apps, actors, components). Both wizards end in a YAML preview you can copy or download into your project.
The dashboard ships as a standalone binary published on GitHub Releases.
Install (one-liner):
macOS / Linux — installs to ~/.local/bin
curl -sSL https://raw.githubusercontent.com/diagridio/dev-dashboard/main/scripts/install.sh | sh
Windows (PowerShell) — installs to %LOCALAPPDATA%\Programs\diagrid-dev-dashboard
iwr -useb https://raw.githubusercontent.com/diagridio/dev-dashboard/main/scripts/install.ps1 | iex
To pin a specific version, set VERSION before piping:
macOS / Linux
curl -sSL https://raw.githubusercontent.com/diagridio/dev-dashboard/main/scripts/install.sh | VERSION=vX.Y.Z sh
Windows
$env:VERSION='vX.Y.Z'; iwr -useb https://raw.githubusercontent.com/diagridio/dev-dashboard/main/scripts/install.ps1 | iex
If the install directory is not on your PATH, the script prints the export line to add.
Install with Go (≥ 1.26):
go install github.com/diagridio/dev-dashboard@latest
Note:
go installnames the binary after the module path, so it producesdev-dashboard, notdiagrid-dev-dashboard— rename it afterwards (mv "$(go env GOPATH)/bin/dev-dashboard" "$(go env GOPATH)/bin/diagrid-dev-dashboard") or use the install script above.
Manual download:
Download the archive for your platform from the GitHub Releases page, extract it, and place diagrid-dev-dashboard (or diagrid-dev-dashboard.exe) on your PATH. Verify with:
diagrid-dev-dashboard --version
# Start on the default port (9090) and open your browser automatically
diagrid-dev-dashboard
# Start on a custom port
diagrid-dev-dashboard --port 8080
# Enable diagnostic logging to stderr (server startup, app discovery, state-store connection, log streams, workflow operations)
diagrid-dev-dashboard --verbose
No additional setup is needed: the dashboard discovers running Dapr apps the same way
dapr list does, so anything started with dapr run / dapr run -f, Aspire, Docker
Compose, or Dapr Testcontainers shows up within one refresh cycle. Testcontainers apps
(e.g. a Spring Boot app under mvn spring-boot:test-run) need zero configuration: the
dashboard finds the Testcontainers-managed daprd container, pairs it with your host app
process, and even reads workflows from an in-memory state store via the sidecar itself.
Tip: once the dashboard is open, try the Konami code (↑ ↑ ↓ ↓ ← → ← → B A) and have some fun! ;)
With --mode/DEVDASHBOARD_MODE unset (the default for host use), the dashboard performs the
complete scan across all discovery sources described above. Setting a mode restricts every
dashboard surface — applications, workflows, state stores, the Control Plane view, and log
targets — to a single source; filters are exclusive and never combined:
--mode dapr-run — host dapr run processes only (Control Plane shows the dapr init containers).--mode compose — Docker Compose containers only (Control Plane shows compose-run placement/scheduler).--mode test-containers — Testcontainers discovery only (no control-plane detection yet).--mode aspire — Aspire resources only. Inside an AppHost-managed container (the
DEVDASHBOARD_APP_* contract is present) this is the container serving posture described
below; on a plain host it filters the process scan to Aspire-managed apps.compose and test-containers require a container runtime (docker or podman) and fail at
startup without one. --bind (default 127.0.0.1, 0.0.0.0 in aspire container posture)
controls the listen address alongside --port.
The dashboard also ships as a container image, purpose-built for embedding inside a
.NET Aspire AppHost via the
diagrid-labs/dashboard-aspire hosting
integration (which will be rewritten against the contract below). It also runs standalone
with a hand-written docker run.
Image: ghcr.io/diagridio/dev-dashboard, tagged :X.Y.Z (in lockstep with binary
releases) and :latest. Prerelease tags (e.g. v1.5.0-rc.1) publish their own version tag
but do not move :latest. The image bakes in DEVDASHBOARD_MODE=aspire and serves on port
8080 bound to 0.0.0.0.
In aspire mode, discovery is restricted to the env contract below (no host process scan, no Docker Compose scan), and the following are disabled: app lifecycle controls (start/stop/restart), the control-plane page, log tailing, self-update/update-check, and automatic browser opening.
Try it:
docker run --rm -p 8080:8080 \
-e DEVDASHBOARD_APP_COUNT=1 \
-e DEVDASHBOARD_APP_0_ID=myapp \
-e DEVDASHBOARD_APP_0_DAPR_HTTP=http://host.docker.internal:3500 \
ghcr.io/diagridio/dev-dashboard:latest
Mode switch:
| Source | Values | Default |
|---|---|---|
--mode flag / DEVDASHBOARD_MODE env | dapr-run, compose, test-containers, aspire | unset (complete scan) |
App discovery — one set of _<i>_* vars per app, for i = 0..DEVDASHBOARD_APP_COUNT-1:
| Env var | Required | Meaning |
|---|---|---|
DEVDASHBOARD_APP_COUNT | yes | number of apps (0 is valid: empty dashboard) |
DEVDASHBOARD_APP_<i>_ID | yes | Dapr app-id |
DEVDASHBOARD_APP_<i>_DAPR_HTTP | yes | daprd HTTP base URL, reachable from the dashboard container (e.g. http://myapp-dapr:3500) |
DEVDASHBOARD_APP_<i>_NAMESPACE | no | per-app Dapr namespace; defaults to DEVDASHBOARD_NAMESPACE. Used for app-scoped workflow operations — fetching one instance's history, an app-filtered workflow list/stats, and force delete — so those honor the app's own namespace. The store-wide workflow list, stats, and app-id dropdown (all-apps scans) still use the global DEVDASHBOARD_NAMESPACE |
DEVDASHBOARD_APP_<i>_LABEL | no | display name; defaults to the app-id. Shown in the applications list and app detail header whenever it differs from the app-id |
Validation is fail-fast at startup: a missing or non-numeric DEVDASHBOARD_APP_COUNT, any
missing required per-app var, or an unparsable DAPR_HTTP URL exits with an error naming the
exact variable.
Security note: in aspire mode the dashboard drops the loopback Host check (the container
is addressed by its Docker-network name or a published port). Without DEVDASHBOARD_ALLOWED_HOSTS
it accepts any Host, which means a malicious web page using DNS rebinding can reach the API even
when the port is published only to localhost. Setting DEVDASHBOARD_ALLOWED_HOSTS to the
hostnames the dashboard is served under closes that hole (loopback names are always allowed).
Mutating requests are still protected by a normalized same-origin check. The dashboard is a local
development tool — never expose it publicly.
Serving and features:
| Env var / flag | Default (aspire) | Meaning |
|---|---|---|
DEVDASHBOARD_PORT / --port | 8080 | listen port |
DEVDASHBOARD_BIND / --bind | 0.0.0.0 | bind address |
DEVDASHBOARD_STATESTORE_FILE / --statestore | unset | path to a mounted Dapr state-store component YAML; enables the Workflows page |
DEVDASHBOARD_NAMESPACE / --namespace | default | default Dapr namespace for workflow actor keys |
DEVDASHBOARD_RESOURCES_PATH | dir of DEVDASHBOARD_STATESTORE_FILE | extra component directories for the Resources page, os.PathListSeparator-separated |
DEVDASHBOARD_ALLOWED_HOSTS | unset (any host) | optional, aspire mode only; comma-separated hostnames the Host header is restricted to (loopback always allowed). Empty means any host. Set it to close the DNS-rebinding hole described above |
DEVDASHBOARD_MODE | aspire (baked into the image) | see mode switch above |
Precedence everywhere is flag > env > posture default.
On startup the dashboard checks GitHub for a newer release. If one exists, it prints a notice as the first line of output and the web UI shows an Update available indicator next to the version number in the Resources panel. The check is best-effort: it is skipped for source/dev builds and fails silently when offline.
Update to the latest release (no-op if already current)
diagrid-dev-dashboard update
Install a specific version (can downgrade or reinstall)
diagrid-dev-dashboard update 1.2.0
update downloads the release archive for your platform, verifies its SHA256
against the release checksums.txt, and atomically replaces the running binary.
Restart any running dashboard to use the new version.
If the dashboard does not behave as expected, run it with --verbose to print diagnostic logs to stderr:
diagrid-dev-dashboard --verbose
Logs are grouped by component= (values: server, discovery, workflow, registry, reconciler) and use levels INFO (normal milestones), WARN (degraded but still working, e.g. a state store that failed to initialise), and ERROR (an operation failed, e.g. the server could not bind its port). Without --verbose, no diagnostic logs are emitted.
Developers use the dashboard to observe and debug Dapr apps while building locally:
<app-id>||<key> — the same key shape
Dapr writes — and refuses a key that already exists unless you opt into overwriting; records can
be deleted individually or by multi-select. There is no in-place edit: overwrite the key instead.
Two limitations worth knowing:
workflow history and actor state are hidden by default (they live in the same keyspace
as your app's records — use Show internal keys to reveal them), and there is no
"last modified" column — Dapr's state components do not expose a modification timestamp,
so none can be shown. The Version column is the backend's etag: it changes on every write,
but it is not a time. Stores that cannot be opened directly — an in-memory store inside a
Testcontainers app, for example — cannot be browsed at all: Dapr's state API has no way to
enumerate keys, so there is nothing to page over.~/.dapr/dev-dashboard/connections.yaml (mode 0600). When more than one store is known, a
selector on the Workflows page lets you switch which one you browse.The UI is built for fast scanning and debugging: deep-linkable views, a global autorefresh control that doubles as a backend-connection indicator (data polling pauses while the backend is unreachable and resumes on recovery), full keyboard operability, and cross-navigation between related entities (app → component → "loaded by" app, etc.).
The dashboard is a local development tool, and only that:
dapr list does — from the local process table and local container runtime —
which has no meaning in-cluster.Run it on your own machine, alongside the apps you start with dapr run, Aspire, Docker
Compose, or Dapr Testcontainers.
Prerequisites: Go ≥ 1.26 and Node.js 20 (with npm). The binary embeds the React SPA via
go:embed, so the web assets (web/dist) must be built before the Go binary — make build
does both in the right order.
macOS / Linux:
make build # builds web/dist, then the Go binary at bin/diagrid-dev-dashboard
./bin/diagrid-dev-dashboard
Equivalent manual steps (if you don't have make):
cd web && npm install && npm run build && cd ..
go build -o bin/diagrid-dev-dashboard .
./bin/diagrid-dev-dashboard
Windows (PowerShell): make is usually unavailable, so run the steps directly:
cd web; npm install; npm run build; cd ..
go build -o bin/diagrid-dev-dashboard.exe .
.\bin\diagrid-dev-dashboard.exe
To build for a sub-path mount, set DASH_BASE_PATH before building (see
Mounting under a sub-path). On Windows that is
$env:DASH_BASE_PATH='/dashboard/' before the npm run build step.
Other useful targets: make test (Go unit + web suites), make test-go, make test-web,
make test-integration, make test-e2e, make tidy.
The dashboard is a passive observer: it discovers your app the same way dapr list does
and reads workflow data directly from your Dapr state store. You don't point it at your app —
you just run both on the same machine.
Prerequisites:
dapr init has been run. This creates ~/.dapr/components/statestore.yaml (a Redis store with
actorStateStore: "true") and starts Redis. That actorStateStore store is what Dapr
Workflows persist to, and what the dashboard reads.Steps:
dapr run --app-id order-processor --app-port 6001 -- <your app start command>
# or, for a multi-app project:
dapr run -f .
./bin/diagrid-dev-dashboard # opens http://localhost:9090
If the Workflows page is empty: the dashboard auto-detects state-store components from
~/.dapr/components and from the live --resources-path of running apps, then uses the one
marked actorStateStore: "true" (falling back to the first detected). Check:
./bin/diagrid-dev-dashboard --statestore ~/.dapr/components/statestore.yaml. You can also add a
store by hand via the connection manager on the Components page.default. For another namespace, pass
--namespace <ns>.state.in-memory, or a Testcontainers app whose store lives
inside the container) are served via their sidecar's gRPC workflow API instead — this
requires Dapr ≥ 1.17 and only works while the sidecar is running.Testcontainers apps (e.g. the Java quickstarts run with mvn spring-boot:test-run) need
none of the store setup above: workflows, components, and app details all come from the
Testcontainers-managed sidecar itself.
There are four suites: Go unit tests, Go integration tests, the web
(frontend) tests, and an opt-in Go e2e suite. The unit and web suites are self-contained.
The integration suite runs its state-store and workflow tests against a temporary SQLite
database plus real Redis, PostgreSQL, and MongoDB containers (via testcontainers-go), so it
needs Docker or Podman for full coverage; without one, the container-backed tests skip. The
e2e suite drives a real daprd and is local-only — it skips automatically when Dapr is not
installed (see below).
Prerequisites: Go ≥ 1.26 (Go tests) and Node.js 20 with npm (web tests).
Run everything (macOS / Linux):
make test # Go unit tests (with -race) + web tests
make test runs make test-go then make test-web. It does not run the Go integration
tests — run those separately (see below).
Go unit tests — gated by //go:build unit:
make test-go # = go test -tags unit -race ./...
# or directly:
go test -tags unit ./...
go test -tags unit -race ./cmd/... # one package, with the race detector
(make test-go uses gotestsum for nicer output if it's installed, otherwise plain go test.)
Go integration tests — gated by //go:build integration; they exercise the state-store and
workflow read paths (including a parity test of the workflow list/stats/detail against all four
supported backends), the parsed sidecar /v1.0/metadata, and the full assembled HTTP server.
Backends run as containers through testcontainers-go (skipped when no container runtime is
available) alongside a temp SQLite DB. They run in CI but are not part of make test:
make test-integration # = go test -tags integration -race ./...
# or directly:
go test -tags integration ./...
Some integration tests use golden files (testdata/golden/*); regenerate them after an
intentional shape change with -update, e.g.
go test -tags integration ./pkg/workflow -run Golden -update.
Go e2e tests — gated by //go:build e2e; they run a real Dapr workflow app under
dapr run and read its state back through the dashboard's own packages, validating against
state authored by a live runtime. They require a local Dapr install (dapr init) — dapr on
your PATH and daprd on PATH or in ~/.dapr/bin — and skip automatically when Dapr is
not found. They are local-only and not run in CI:
make test-e2e # = go test -tags e2e ./...
Web tests — Vitest:
make test-web # = cd web && npm install && npm test (vitest run)
# or from web/:
cd web
npm install
npm test # single run
npm run test:watch # watch mode
Windows (PowerShell) — make is usually unavailable, so run the commands directly:
go test -tags unit -race ./...
go test -tags integration ./...
cd web; npm install; npm test; cd ..
Tip: the Go tests are build-tag-gated, so a plain
go test ./...(without-tags unit,-tags integration, or-tags e2e) reports "no test files" for most packages. Always pass the tag.
make lint # lint-go (gofmt + go vet) + lint-web (eslint)
make lint-go # = gofmt check + go vet -tags unit ./...
make lint-web # = cd web && npm install && npm run lint (eslint .)
The Go checks (gofmt, go vet) and the web eslint run in CI on every push and pull request.
Pre-commit hook (optional): install a hook that lints only your staged files before each commit:
make hooks # symlinks .git/hooks/pre-commit -> scripts/pre-commit
It runs gofmt/go vet on staged Go files and eslint on staged web/ files. Bypass it for a
single commit with git commit --no-verify.
For maintainers with push access. Releases are built and published by the
releaseGitHub Actions workflow: pushing avX.Y.Ztag runs GoReleaser, which compiles the cross-platform archives pluschecksums.txtand publishes them to a GitHub Release. The version (diagrid-dev-dashboard --version) is injected from the tag via build-time ldflags. The same tag-driven run also builds and pushes the multi-arch container image toghcr.io/diagridio/dev-dashboardvia goreleaser. Prerelease tags (any-suffix, e.g.v1.5.0-rc.1) push their version-tagged image but skip the:latestmanifest, so release candidates never reach:latestconsumers.
Because go install cannot run npm, the release tag commit must embed the prebuilt
web/dist. scripts/release.sh handles this: it builds the SPA, creates a detached commit
that force-adds web/dist (past .gitignore), tags it, and returns you to your branch — so the
tagged commit ships the full UI for go install while main stays free of built assets.
Cut a release (macOS / Linux, or Git Bash / WSL on Windows — release.sh is a POSIX sh script):
main with a clean working tree.scripts/release.sh vX.Y.Z
It builds the SPA, creates the tag on a detached commit embedding the UI, and prints the
push command.git push origin vX.Y.Z
release workflow to finish. It publishes a GitHub Release with one archive per
platform + checksums.txt. After that, the install one-liners and
go install github.com/diagridio/dev-dashboard@vX.Y.Z resolve to the new version.Validate locally before tagging (optional, requires GoReleaser v2):
make release-check # validate .goreleaser.yaml
make release-snapshot # build a local snapshot into dist/ without publishing
The release matrix is 5 archives: macOS and Linux (amd64 + arm64) and Windows (amd64). There is no native Windows/arm64 build — Windows on ARM uses the amd64 build via emulation.
The dashboard is a single Go binary that embeds a React SPA and talks to your local Dapr sidecars and state store.
For the full architecture — how discovery, state-store election, the reconciler, the HTTP layer, and the SPA fit together, plus a guide to extending each part — see ARCHITECTURE.md. The summary below is the quick tour.
┌───────────────────────────────────────────────────────────────┐
│ diagrid-dev-dashboard (single Go binary) │
│ │
│ cmd/ cobra root, flags, serve boot, │
│ connection registry + reconciler │
│ pkg/server chi router + go:embed SPA │
│ pkg/discovery standalone.List() + /v1.0/metadata │
│ pkg/workflow list / history / purge │
│ pkg/state state record listing / add / delete │
│ pkg/statestore client (redis / postgres / sqlite / mongodb) │
│ pkg/controlplane docker/podman inspect + lifecycle │
│ pkg/metadata component metadata catalog │
│ pkg/resources component + configuration YAML loader │
│ pkg/logs file tail → SSE │
│ web/ React + Vite SPA → dist/ (embedded) │
└───────────────────────────────────────────────────────────────┘
│ HTTP /v1.0/* │ files / TCP │ docker/podman
▼ ▼ ▼
running daprd ~/.dapr, resource paths, control-plane
sidecars state store backend containers
Components
chi) — exposes a REST + JSON API (with SSE for log/stream tails) and
serves the embedded SPA. Each domain lives in an isolated pkg/* package (a service.go
plus its response types); the HTTP layer lives in pkg/server, one file per domain. No
domain package depends on cmd/.go:embed. Uses TanStack Query for polling/caching,
headless accessible primitives styled in-house, and a custom lightweight read-only YAML
highlighter (not Monaco). Client-side History-API routing (react-router-dom); the Go
server falls back to index.html for unknown paths and is base-path-aware. List
virtualization is planned, not yet in v1. The UI styling conventions (design tokens,
page anatomy, component classes) are documented in
web/STYLEGUIDE.md.Key dependencies & data sources
github.com/dapr/cli/pkg/standalone (the same mechanism as
dapr list). standalone.List() reads the local process table and is the source of truth
for existence/ports/PIDs; the /v1.0/metadata call per sidecar is enrichment (runtime
version, components, actors, subscriptions, extended metadata) and degrades gracefully when
a sidecar is down. A /v1.0/healthz check per sidecar drives the health badge — computed
on demand during each /api/apps fetch (no separate background poller), so its refresh cadence
follows the UI's autorefresh interval. A second scanner discovers Dapr apps running under
docker compose by inspecting compose-labelled containers (app id and ports from the daprd
argv, logs streamed from the container runtime). A third scanner discovers Dapr
Testcontainers sessions (org.testcontainers-labelled daprd containers, e.g. from
dapr-spring-boot-starter-test): the randomly-published HTTP/gRPC ports are re-read every
poll, the host app process is paired via the app port (real PID, uptime, runtime), and the
component YAML declared in test config is extracted from the container so it appears on the
Components page. All sources are merged, so one failing never hides the others.state.in-memory included), or any app when no store is openable — workflows are read live
from the sidecar's gRPC workflow API (Dapr ≥ 1.17) instead, per app. Purge uses the
official Dapr workflow API when reachable, with direct state-store key deletion as an
explicit force fallback.~/.dapr/dev-dashboard/connections.yaml (mode 0600). The pkg/metadata catalog drives the
add/edit forms, the workflow backend connects to the selected store lazily (on demand), and
secretKeyRef metadata is resolved through local secret stores
(secretstores.local.file / secretstores.local.env).~/.dapr and live
--resources-path directories read from daprd args.~/.dapr/logs/* and the appLogPath/daprdLogPath reported in
extended metadata, then streamed to the SPA over SSE.docker, else
podman): dapr_scheduler / dapr_placement are the self-hosted containers the dashboard
can start/restart/stop (allowlisted to those names). Container logs stream over SSE via
docker logs -f.GET /api/news endpoint so the SPA only ever talks to its
own origin.Portability — all logic lives in pkg/* domain packages with no dependency on cmd/,
the server mounts as a chi sub-router, and the SPA is an embedded fs.FS, so the whole
thing can later be re-mounted under a diagrid dashboard subcommand.
For the full architecture and extension guide, see ARCHITECTURE.md; for the
original design rationale, see
docs/superpowers/specs/2026-06-25-dev-dashboard-design.md.
Contributions are welcome! See CONTRIBUTING.md for how to report issues,
set up a development environment, and submit pull requests. All commits must be signed off
per the Developer Certificate of Origin (git commit -s).
Copyright © Diagrid Inc. Licensed under the Apache License 2.0.
TypeScript
51.7%
Go
45.3%
CSS
2.3%