better-push
Operate in production

Studio

Monitoring, forensics, and operator actions over the rows better-push already writes.

Every question an operator asks is already a row in your database. bp_notification records what was created, bp_delivery records every attempt with its status, error code, and timings, bp_device records the fleet and why tokens died, bp_preference records opt-outs, and bp_job records queue depth, retries, and dead letters.

The studio is the console over those rows. It is not an instrumentation problem - it is a query problem, which is why better-push can answer it without a metrics service, an agent, or a copy of your data anywhere else.

What it is for

The differentiated value is forensics and queue health - the two things an external APM cannot do, because it does not have these rows:

  • "User 8f2a says the order-shipped push never arrived." Search the user, see their devices, see that one was disabled three days ago with expired_token, see the delivery row that says so.
  • "What is stuck, and why?" Queue depth, the age of the oldest pending job, dead-lettered jobs with their last error, and which worker holds which claim.

It is deliberately not a rebuild of Grafana. There are enough charts for context and no more; aggregate dashboards over long time ranges are what APM tools already do well.

Two ways to run it

CLI studioMounted studio
Commandnpx @better-push/cli studiostudioHandler(push, { … })
Runs onyour machine, localhost onlyyour app, wherever you mount it
Authnone, by designyour own admin session
Contentalways visiblerequires the content permission
Writes--allow-writesallowWrites and the write permission

Both serve the same prebuilt SPA and the same JSON API, so what you learn in one applies to the other.

Screens

  • Overview - volume, success rate, failures by error code, delivery lag, queue depth, live workers, device fleet, latency percentiles, read rate. Selecting an error code opens it in the deliveries explorer.
  • Deliveries - every attempt, with a detail pane beside the list showing the notification and its sibling deliveries.
  • Queue - pending and dead jobs with retry and purge, and each job's payload and last error. It reads through the configured backend's own inspector, so it is identical on dbQueue() and bullmq() and still works with no queue configured at all, where it shows the bp_job table.
  • Users - devices (including disabled ones), stored preference overrides, and recent notifications. This is the screen that answers a support ticket.
  • Send test - go through the real notify() path.
  • Audit - every privileged action, including each content reveal.

Three things apply across all of them:

  • A global time window in the header (6 hours to 90 days) that scopes the overview and the deliveries explorer.
  • A command palette on ⌘K. Paste a notification, delivery, device or user id and it resolves to whatever it is; it also jumps between screens, as does g followed by a letter.
  • Addressable views. The screen, the window, and every list filter live in the URL, so …/deliveries?error=BadDeviceToken&range=7d is a link you can hand to whoever asked.

What it is built on

The UI talks to a StatsSource interface and nothing else:

import { databaseStats } from "@better-push/core/stats";

const stats = databaseStats(drizzleAdapter(db));
await stats.overview({ from: "2026-07-01T00:00:00Z" });

Every method is time-bounded and paginated with a hard server-side cap, so no query the studio can be talked into issuing scans more of your history than the cap allows. Charts read rollups; forensic lists read raw rows through indexes added for exactly that purpose.

The UI reads only through this bounded interface; it does not query tables directly.

What it does not do

  • No HTTP request logging. A feed polling every 30 seconds would write more rows than the notification data itself. Use the onEvent hook to forward lifecycle events to OpenTelemetry or your APM instead.
  • No realtime. Screens poll and show "updated Ns ago".
  • No alerting, and no cross-project views.

On this page