The Dashboard
Mount BackWave's built-in operations Dashboard, secure it with delegated permissions, understand what each operator action does, and enable Pro Workflows.
BackWave ships a built-in operations Dashboard: a set of server-rendered screens for watching your jobs and acting on them, with no separate service to run. It mounts onto your host's own ASP.NET Core pipeline as middleware, reads exclusively through the Monitor, and writes exclusively through the audited Operator. It is read-only the moment you mount it: viewing is allowed, but every write and every sensitive-data view is denied until you opt in.
This page is the whole story for running the Dashboard: mount it, secure it, understand what the operator actions actually do, and enable Pro Workflows. It does not catalogue every screen and column. The screens are self-describing; what follows is what you cannot see by looking.
Mounting the Dashboard#
You mount it with one middleware call, UseBackWaveDashboard, placed after your authentication and authorization middleware so the permission callbacks run against an authenticated request.
builder.Services.AddBackWave(bw => bw
.UseStore(new PostgresJobStore(connectionString))
.UseJobs(BackWaveJobs.Module)
.AddWorkerGroup(new WorkerGroupOptions
{
Name = "default",
Policy = new DispatchPolicy.Strict("emails"),
}));
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseBackWaveDashboard(); // served at /backwave, read-only by default
app.Run();The first argument is the path prefix, defaulting to "/backwave" (it must start with '/' and be non-empty, or the call throws). The second is an optional BackWaveDashboardOptions; omit it and you get the safe read-only defaults. There is no separate AddBackWaveDashboard(). The Dashboard only needs AddBackWave (which registers the Monitor and Operator it resolves at request time) and, once you grant any write permission, a call to AddAntiforgery(). Grant a write without it and the action throws an InvalidOperationException at request time. A purely read-only Dashboard never touches antiforgery.
Every screen reads through the Monitor, so what the Dashboard shows and what your tests assert against the Monitor agree by construction. Every change goes through the Operator as an audited, antiforgery-protected transition, never a raw row edit.
Securing it: permissions as delegation#
BackWave owns no users and no roles. Every permission is a callback you supply that takes the request's HttpContext and returns ValueTask<bool>; BackWave never inspects identities itself, it asks your host whether the current request may do a thing. That single idea, authorization as delegation rather than ownership, is the whole model.
Reads default to allowed so the Dashboard works the instant you mount it (a local-dev convenience). Every write and the sensitive-content gate default to denied. Because a control renders only when its permission passes, a denied user does not see a disabled button. They see no button at all.
builder.Services.AddAntiforgery(); // required once you grant any write
app.UseAuthentication();
app.UseAuthorization();
app.UseBackWaveDashboard("/backwave", new BackWaveDashboardOptions
{
// Read gate for the whole Dashboard (default: allow). Set this in production.
AuthorizeView = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops")),
// Write actions (default: deny). Map each to your own policy or role.
AuthorizeRequeue = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),
AuthorizeCancel = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),
AuthorizePauseQueue = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),
AuthorizeTriggerSchedule = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),
// Raw-content read gate (default: deny).
AuthorizeViewSensitiveData = ctx => ValueTask.FromResult(ctx.User.IsInRole("ops-admin")),
// Stamped into every audited action (default: user name, else "dashboard").
ResolveActor = ctx => ctx.User.Identity?.Name ?? "automation",
});The role checks are illustrative. To delegate to a named ASP.NET Core policy instead, resolve IAuthorizationService from ctx.RequestServices inside the callback and return result.Succeeded. The callback shape is the only contract.
| Option | Default | What it controls |
|---|---|---|
AuthorizeView | allow | Whether the request sees the Dashboard at all. Checked first; a denial returns a 403 and zero bytes. |
AuthorizeViewSensitiveData | deny | Whether raw content (payload, Job Output, Failure Detail) is shown. |
AuthorizeRequeue | deny | The Requeue action. |
AuthorizeCancel | deny | The Cancel action (and Pro's Cancel Workflow). |
AuthorizePauseQueue | deny | Governs both pausing and resuming a Queue. There is no separate resume permission. |
AuthorizeTriggerSchedule | deny | Triggering a Recurring Schedule now. |
ResolveActor | user name, else "dashboard" | The actor string recorded in every audit record. Not a gate. |
There are four write permissions but five write actions, because AuthorizePauseQueue covers both pause and resume. AuthorizeView is the single entry door: it gates GET pages, POST actions, and the live stream alike.
The sensitive-data gate#
Raw content that may carry secrets or PII (job payload, Job Output, and inline Failure Detail) sits behind its own read gate, separate from AuthorizeView. Two conditions must both be true before a single byte is read: the per-request AuthorizeViewSensitiveData permission, and the host-wide ExposeSensitiveData flag (default true). Either one turns content off.
For an ops-side override with no redeploy, set the environment variable BACKWAVE_DASHBOARD_DISABLE_SENSITIVE_DATA to a truthy value (1, true, yes, or on); it forces exposure off regardless of the code flag. This gate governs what an operator views; it does not restrict your handler code from reading an ancestor's output in-process.
Every write action is stamped with the resolved actor and recorded in an append-only audit trail, readable programmatically through BackWaveOperator.ListAuditRecordsAsync for one target (a job id, Queue name, or schedule id).
What operators can do#
Reading the Dashboard tells you what the system is doing; an operator action changes it. Each is a defined state-machine transition, never a raw row edit, stamped with the acting identity and written to the audit trail. The Dashboard buttons are one way to invoke them; the same actions are a public API on BackWaveOperator (registered by AddBackWave, namespace BackWave.Operations), so a runbook can do exactly what an operator does by hand.
The behavior below is the part you cannot infer from the buttons:
| Action | What it actually does |
|---|---|
| Requeue | Valid only when the job is Dead-Lettered or Quarantined. Returns it to Scheduled, resets the attempt count to zero, and sets Due Time to now. The whole attempt budget resets, so the job starts over rather than getting one more try. Any other state is rejected unchanged. |
| Cancel | A not-yet-started job (Scheduled, AwaitingParent) is cancelled immediately. A running (Leased) job cannot be stopped by force. Cancel is cooperative: it sets a flag, and the handler's CancellationToken fires on the next heartbeat. Threads are never killed; the job reaches Cancelled when the handler winds down. |
| Pause / Resume Queue | Pausing stops every Worker in the cluster from claiming new work from that Queue; jobs already running keep running. Resume reverses it. Cluster-wide, no precondition; a no-op still writes an audit record. |
| Trigger Schedule now | Mints one instance right now, due immediately. The schedule's cursor and recorded ticks are untouched, so its normal cadence fires exactly as it would have, a one-off alongside the schedule rather than a shift of it. |
using BackWave.Operations;
public sealed class IncidentResponder(BackWaveOperator backwave)
{
public async Task RecoverAsync(Guid deadJobId, string queue)
{
await backwave.RequeueAsync(deadJobId, actor: "alice@corp"); // dead -> Scheduled, attempts reset
await backwave.CancelJobAsync(deadJobId, actor: "alice@corp"); // cooperative if running
await backwave.PauseQueueAsync(queue, actor: "alice@corp"); // stop future claims, cluster-wide
await backwave.ResumeQueueAsync(queue, actor: "alice@corp");
}
}Each write runs a fixed sequence: the permission is checked (403 on deny), the antiforgery token is validated (400 if missing or invalid), the audited action runs, and the browser is sent a 303 redirect back so a refresh never re-submits. Editing a payload is deliberately not an operator action.
Live refresh#
The at-a-glance screens refresh in place over a Server-Sent Events connection, so figures stay current without reloads. The server re-reads the Monitor on a fixed cadence and pushes an update only when the rendered screen actually changed. LiveRefreshInterval controls that cadence (default 4 seconds); raise it to ease load, lower it for a snappier feel. Live refresh is progressive enhancement. Without it, the server-rendered page still stands on its own.
app.UseBackWaveDashboard("/backwave", new BackWaveDashboardOptions
{
LiveRefreshInterval = TimeSpan.FromSeconds(10), // default is 4 seconds
});Pro: Workflows on the Dashboard#
The free Dashboard ships with no Workflow UI. The BackWave Pro dashboard package adds a Workflows list and an interactive graph view of a single Workflow's members, contributed through the Dashboard's extension seam without the free Dashboard depending on it. Enable it with one registration, AddBackWaveProDashboard(), called after AddBackWave(...).AddBackWavePro(...):
builder.Services
.AddBackWave(/* ... */)
.AddBackWavePro(builder.Configuration["BackWave:ProLicense"]);
builder.Services.AddBackWaveProDashboard();There is no Pro-specific mounting step; the free Dashboard is still mounted the one way it always is. The Workflows nav entry slots in after Failures, the Cancel Workflow action reuses the existing Cancel permission (no new permission), and member payload and output reuse the same sensitive-data gate as everywhere else.
The Workflows surface appears because the package is registered, not because the license is valid. The license drives exactly one thing: a soft amber banner when the license is missing, malformed, or out of term. Every Pro feature runs identically regardless, because the software cannot and does not detect revenue. Pro is free for organizations under $1M annual revenue on the honor system; the banner is a reminder, not a restriction.
Where to go next#
- The Monitor API: the read surface every screen renders, for building your own views, alerts, and tests.
- Health, Fail-Stop & Draining: what happens at the runtime level when a node dies or shuts down.
- Scaling & Throughput Tuning:
PoolSize,Pumps, and when to add nodes. - Retention & History Purge: how long jobs and transitions are kept before purge.
- Workflows: the Pro grouping the Workflows surface visualizes.