Skip to main content

Dashboard

@portqueue/dashboard is a pluggable React ops dashboard — queues, jobs, schedules, instances and live stats. Mount it on your existing HTTP server (Express, Koa, Fastify, NestJS or raw Node), or serve it from the CLI with no app changes at all.

Pages​

Navigation lives in a left sidebar (it collapses behind a burger button on small screens), with links to these docs and the GitHub repo pinned at its foot, and the live-updates control and the light/dark toggle in the top bar. Live updates refresh the current page's data every 15 seconds and pause with the tab hidden; the control freezes them, and a refresh button fetches on demand either way. Every page opens with a full-width subheader carrying its title, a one-line description, and the page's actions.

The screenshots below follow the theme you are reading in — switch it in the top-right and they swap with the rest of the site. Click any of them to see it full size.

Overview & queue details​

The overview opens on activity cards — active, pending, failed and completed — above a table of every queue with its state, usage and per-status counts, pause/resume on each row, and the name linking through to the queue itself.

A queue's own page narrows the same cards to that queue and lists its jobs: paged and searchable, with per-row retry, cancel and remove plus bulk actions over a selection. Pause/resume, Add job and (for a queue no running process declares) Remove queue sit in the subheader. The Add job form mirrors addJob, so its timing fields are the same seconds the API takes.

Job details​

One job's full record — queue, status, priority, attempts, the instance that processed it, lock, idempotency key and schedule — beside its data, return value, failure message and a lifecycle timeline, followed by every lifecycle timestamp including the ones the job never reached.

Retry, clone, cancel and remove sit in the subheader. Two of them leave the page: a clone opens the new job's (or schedule's) own page, and a removal returns to the queue. A running job is never removed — cancel it first.

Instances list & details​

One row per registered instance: liveness, the CPU and memory of that Node process at its last heartbeat, jobs processed, and pause/resume. Both percentages are process-local — CPU a share of the cores available to it, memory a share of V8's heap limit — and turn amber from 75% and red from 85%. Rows only appear for servers that set an instance key.

Each key opens its own page: the same health as cards, plus the jobs that instance claimed, with pause/resume and remove (available once it has been unseen for 30 minutes). A job's Processed on instance links here too.

Schedules list & details​

Recurring definitions across every queue — cadence, timezone, next and last run — with per-schedule enable/disable and bulk unschedule. Disabling keeps the definition and stops it firing, the reversible half of "make this stop".

A schedule's page shows the full definition (including its idempotency key) above the jobs it has produced, and offers disable and remove. Removing returns to the list, since the definition is gone.

Mount on an existing app​

Install the dashboard package alongside @portqueue/core:

npm install @portqueue/dashboard
Your driver's client is still required

The dashboard reads through a @portqueue/core driver, so its client (pg, better-sqlite3, @electric-sql/pglite, …) must be installed too — otherwise PortQueue throws MissingPeerDependencyError.

Create the dashboard once from a driver — not from your running PortQueue — then mount it with the adapter for your framework. Express, Koa, Fastify, NestJS and raw Node all ship in the box.

It builds its own client

createDashboard({ driver }) constructs and owns a small headless PortQueue behind the scenes. That is what lets the dashboard run wherever you like: in almost every real deployment it is a different process from the workers, so riding on their client only ever worked in the one case where the two happened to share a process.

Headless means it observes without joining: it declares no queues, registers no instance row, runs no workers or scheduler, and never migrates your schema. Nothing connects at mount either — the first request opens the connection, and dash.close() releases it.

One consequence worth knowing: the queues table has no "in use" column. That flag means "declared in this process", which a separate dashboard cannot know. pq.getQueues() still reports it for your own application code.

import { createDashboard } from '@portqueue/dashboard';

const dash = createDashboard({ driver, basePath: '/portqueue' });

// `basePath` is the mount point, so the middleware goes on at the root.
app.use(dash.express());

driver is the only required option. basePath defaults to /portqueue, and logger (default console) is where the dashboard reports the one problem it can only notice at startup: a missing client bundle, which otherwise serves a placeholder with a 200 and looks like a working deploy.

basePath is the mount point

It tells the dashboard where its own assets and API sit, so it must match where you mount the adapter. Only NestJS repeats it — keep forRoutes(...) in sync.

The dashboard ships no authentication

Anyone who can reach the mount can pause queues, remove jobs and delete schedules. Protecting it is your application's job — see Protecting the dashboard below.

Protecting the dashboard​

The dashboard has no authentication of its own, and no read-only mode: anyone who can reach the mount can pause queues, retry and remove jobs, and delete schedules. Guarding a mount is something your application already does, so bring your own middleware and run it in front of the adapter — the same session, SSO or API-key check that protects the rest of your admin surface. Anything that runs first and can end the response works; the dashboard only ever sees what got past it. Shipping a second, weaker mechanism alongside yours would only invite people to rely on it.

One thing the mount does bring itself: a mutation is refused unless it carries the x-requested-with: portqueue-dashboard header the SPA sends. A browser cannot set a custom header cross-origin without a preflight this mount never answers, so another page an operator visits cannot pause a queue with their session — which matters precisely because your middleware is what a browser replays cookies and Basic credentials to. It is not authentication, and there is nothing to configure; if you script against the JSON API yourself, send that header on anything that writes. Reads need nothing.

No auth layer to reuse? HTTP Basic in front of the mount is enough for an internal tool — basic-auth parses the header and its README has the middleware; compare the credentials with timingSafeEqual from node:crypto, terminate TLS in front of it, and rate-limit the mount if it is reachable from the internet. Or skip your app entirely and let the CLI host the dashboard — it brings its own Basic auth, below.

Serve it from the CLI (no app changes)​

If you'd rather not touch your app, the CLI hosts the dashboard as a standalone process. It opens its own short-lived connection from --driver + --connection-string (or DATABASE_URL) and serves the same UI.

The CLI is the one case with no host application to put a guard in front of, since it opens the port itself — so it brings its own HTTP Basic auth, and credentials are required: pass --root-user / --root-password or set PORTQUEUE_ROOT_USER / PORTQUEUE_ROOT_PASSWORD. The bind defaults to --host 127.0.0.1 (loopback only), with --port 4321 and --base-path /portqueue:

portqueue dashboard --driver postgresql \
--connection-string "$DATABASE_URL" --port 9000 \
--root-user admin --root-password "$PORTQUEUE_ROOT_PASSWORD"
Wrap the connection in a small script

So you don't retype the connection on every command, put it in a one-line shell script and call that instead — see Reuse the connection with a script on the CLI page.

Docker​

You don't need to containerise your app to run the dashboard — the CLI image can host it as its own service. Pass the connection via DATABASE_URL and expose the dashboard --port.

Dockerfile
FROM node:22-alpine
WORKDIR /app

# Install the CLI plus the client for your driver — it's an optional peer dependency,
# so pick the one you use: pg (PostgreSQL), mysql2, oracledb, …
RUN npm install -g @portqueue/cli pg

EXPOSE 9000
# Serve the dashboard. DATABASE_URL and the dashboard credentials
# (PORTQUEUE_ROOT_USER / PORTQUEUE_ROOT_PASSWORD) are supplied at run time (see below).
ENTRYPOINT ["portqueue", "dashboard", "--driver", "postgresql", "--port", "9000"]
Serve the dashboard
# Build the image once.
docker build -t portqueue-dashboard .

# Run it: pass the connection string and dashboard credentials, publish the port.
docker run --rm -p 9000:9000 \
-e DATABASE_URL="postgres://user:pass@db:5432/app" \
-e PORTQUEUE_ROOT_USER="admin" \
-e PORTQUEUE_ROOT_PASSWORD="<your-password>" \
portqueue-dashboard