Workhorse
Integrations

Dashboard

Mount the operator interface behind application authorization, or run it standalone with a built-in administrator login.

@stablemates/workhorse-dashboard gives your operators a full interface — jobs, attempts, checkpoints, schedules, queues, workers, failures, health — without you building a single admin page. It reads everything from PostgreSQL, so a dashboard mounted anywhere can observe workers running on machines it has never met.

What it shows

Every view comes from the same read models as Queries and timelines: payload-free job listings, per-job timelines with redacted payloads, dead letters, the worker registry, and the health verdict with its reasons. With host-authorized controllers wired in, it can also cancel jobs, run schedules now, pause queues and workers, and edit maintenance and retention policy.

Mount it in your server

createDashboardHost builds a framework-neutral handler over standard Request/Response objects. It answers only for requests under its configured path and returns null for everything else.

import { createDashboardHost } from "@stablemates/workhorse-dashboard/server";

const host = createDashboardHost({
  path: "/workhorse",
  database: pool,
  environment: "production",
  authorize: (request) => isOperator(request),
});

Fetch-native frameworks — Hono, Next.js route handlers, SvelteKit, Nitro — call host.handle(request) and fall through when it resolves to null:

app.all("/workhorse/*", async (c) => {
  const response = await host.handle(c.req.raw);
  return response ?? c.notFound();
});

Connect-style frameworks — Express, Connect, Fastify via @fastify/middie — wrap the same host:

import { dashboardNodeMiddleware } from "@stablemates/workhorse-dashboard/server";

app.use(dashboardNodeMiddleware(host));

The middleware passes unowned requests to next() untouched, so the dashboard never captures unrelated routes. On every owned request, the host checks schema compatibility and refuses with a clear error rather than serving against a mismatched database. It never installs or migrates anything.

Serve several databases as workspaces

One dashboard can serve several Workhorse databases as named workspaces, switchable from the header. Configure workspaces instead of database; each entry owns its connection and may override environment, operator, and the controllers, falling back to the host-level options:

const host = createDashboardHost({
  path: "/workhorse",
  workspaces: {
    production: { database: productionPool, environment: "production" },
    staging: { database: stagingPool, environment: "staging" },
  },
  defaultWorkspace: "production",
  authorize: (request, workspace) => isOperator(request, workspace),
});

Each workspace lives under its own path — /workhorse/production, /workhorse/staging — so links deep into one workspace stay shareable and two browser tabs can watch two workspaces. The authorize callback receives the resolved workspace name, letting the application grant access per workspace. The host still never owns pool sizing, shutdown, or credentials for any of the connections.

The standalone CLI serves workspaces too: repeat --workspace name=url, or point --config at a JSON file whose entries name a url directly or a urlEnv environment variable that keeps credentials out of the file.

Choose the authentication boundary

An embedded dashboard uses your application's authentication through authorize. The callback runs before the interface, assets, and every RPC method. Return a verified principal for an authenticated operator, false for a 403, or your own Response to redirect to a login flow.

Mutations go further. The operator, queueController, taskController, workerController, scheduleController, and settingsController options let each mutation pass through your own authorization and audit services. auditActor and the requestedBy fields provide attribution after authorization — they record who acted, they never decide who may.

The settings page edits maintenance and retention policy through DashboardSettingsController, previews destructive retention changes before applying them, and shows process-owned worker values read-only, because changing those requires a deployment.

The standalone CLI

For a quick local console, skip the mounting entirely:

workhorse dashboard --port 3000

It binds 127.0.0.1 and serves read-only by default. Without credentials, that loopback listener is an explicit development bypass and cannot bind to a remotely reachable address.

Set WORKHORSE_DASHBOARD_USERNAME and WORKHORSE_DASHBOARD_PASSWORD_HASH to enable the built-in administrator login. The standalone server owns session expiry, logout, password rotation, origin checks, and login throttling. A remote listener also requires an HTTPS public origin. Installations that need multiple users, roles, SSO, or tenant isolation should use an embedded host and its authorize callback.

The login page follows the browser's color scheme. The dashboard header shows the authenticated administrator and provides sign-out; if the session expires while the page is open, the next private request returns the browser to login.

Customize without forking

Hosts that want the components inside their own React tree can render Dashboard and WorkhorseThemeProvider directly, connected through createDashboardClient, importing the packaged stylesheet for tokens and layout. configuredWorkers describes expected worker processes before they first register, and projectDurability translates checkpoint evidence into domain-specific step names without touching stored queue data.

Refresh model

The browser polls only the active page at the selected cadence, so job volume never multiplies browser requests; manual refresh covers the rest. Remember what you are looking at: an operator view, not a transactional snapshot. PostgreSQL statistics and worker registry rows can lag the processes behind them by design.

Next


Exact operator projection, worker registry, and read-model behavior: architecture reference.