Client Reporting Dashboards for the Apps and SaaS You Build for Clients
If your agency or freelance practice builds apps, SaaS products or platforms for clients, sooner or later every client asks the same thing: can I see how it's doing? Bookings, orders, signups, revenue, whether it's up. The numbers are already in the database you designed, behind the API you wrote. The expensive part is the client dashboard itself, which you'd otherwise have to code, host and maintain inside every client's product.
This page is for that situation: a client reporting dashboard for software you build, without a frontend to write. It covers which widgets to put on it, the JSON each endpoint returns, and how to set it up per client. If you're a marketing agency reporting on ads and SEO, this isn't the right tool; our client dashboard software comparison covers the tools built for that.
Why not build the dashboard into the client's app?
You can, and for some clients you should (more on that below). But a reporting page inside each client's product is a small project every time:
- Charts, layout and a mobile view, for numbers that change shape whenever the client asks a new question.
- Auth and roles, because the client's managers shouldn't see what their admins see.
- Hosting and upkeep: chart library upgrades, a broken query after a schema change, a TV in the client's office that needs a kiosk login.
- Hard to bill. Clients pay happily for features; they don't like paying for the dashboard's maintenance.
With dashboardbase, each widget is one small read-only endpoint in the client's backend. You write the query, dashboardbase renders it on the web, on phones and on a TV. The long version of the build-it-yourself trade-off applies doubly when you'd build it once per client.
What goes on a client reporting dashboard
Six widgets cover most clients. Swap the business numbers for whatever the app is about: bookings, orders, deliveries, active subscribers.
| Widget | Type | What the client reads from it |
|---|---|---|
| Bookings today | KPI with trend | Is today normal? |
| Signups, last 7 days | Line chart | Is the launch or campaign working? |
| Revenue this month | KPI with goal bar | Are we on track? |
| App health | Status heartbeat | Is it up? (The first thing they'd otherwise email you about) |
| Open issues | KPI, red when rising | Is the agency on top of it? |
| Retainer hours used | Progress list | What's left this month, from your own time tracker |
The last one is worth noticing. A dashboard can mix endpoints from different systems, so the client's numbers and your agency's numbers sit on one screen.
The JSON, widget by widget
The response is the widget object itself, with no wrapper. Your endpoint fills in the values.
Bookings today:
{
"title": "Bookings today",
"actions": [
{
"title": "View Bookings",
"type": "link",
"url": "https://example.com/admin/bookings"
}
],
"data": {
"header": {
"title": "62",
"subtitle": "vs same day last week",
"badge": {
"text": "+12%",
"icon": "ArrowUp",
"color": "Success"
}
}
}
}Signups over the last 7 days:
{
"title": "Signups",
"actions": [
{
"title": "View Users",
"type": "link",
"url": "https://example.com/admin/users"
}
],
"data": {
"header": {
"title": "418",
"subtitle": "Last 7 days",
"badge": {
"text": "+31%",
"icon": "ArrowUp",
"color": "Success"
}
},
"labels": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
"datasets": [
{
"data": [
{ "value": 41 },
{ "value": 55 },
{ "value": 62 },
{ "value": 70 },
{ "value": 66 },
{ "value": 58 },
{ "value": 66 }
],
"label": "Signups"
}
]
}
}Revenue this month, against the client's goal:
{
"title": "Revenue this month",
"actions": [
{
"title": "View Details",
"type": "link",
"url": "https://example.com/admin/revenue"
}
],
"data": {
"header": {
"title": "$24,300",
"subtitle": "toward $30k goal",
"badge": {
"text": "+$2,150",
"icon": "ArrowUp",
"color": "Success"
}
},
"progress": {
"value": 24300,
"max": 30000,
"label": "Goal $30,000"
}
}
}App health, beating on the office TV:
{
"title": "App health",
"actions": [
{
"title": "Check Logs",
"type": "link",
"url": "https://example.com/status-logs"
}
],
"data": {
"header": {
"title": "Operational",
"subtitle": "All checks passing"
},
"status": "Ok",
"mode": "Heartbeat"
}
}Retainer hours, from the agency's own system:
{
"title": "Retainer hours",
"actions": [
{
"title": "View Timesheet",
"type": "link",
"url": "https://example.com/timesheet"
}
],
"data": {
"header": {
"title": "October",
"subtitle": "Hours used of the monthly retainer"
},
"items": [
{ "value": 18, "max": 24, "label": "Development" },
{ "value": 5, "max": 8, "label": "Support" },
{ "value": 2, "max": 4, "label": "Hosting and ops" }
]
}
}Paste any response into the public endpoint validator to see the real widget before wiring it up; no account needed. More widget types are in API dashboard examples.
One endpoint, in the client's backend
This goes in the codebase you already maintain for the client. It assumes a bookings table with a created_at column:
import { timingSafeEqual } from "node:crypto"
// Fail closed at startup rather than serving the client's numbers to anyone.
const SECRET = process.env.DASHBOARDBASE_ENDPOINT_SECRET
if (!SECRET) throw new Error("DASHBOARDBASE_ENDPOINT_SECRET is not set")
const KNOWN = Buffer.from(SECRET)
function secretMatches(header: string | undefined): boolean {
if (header === undefined) return false
const sent = Buffer.from(header)
return sent.length === KNOWN.length && timingSafeEqual(sent, KNOWN)
}
app.get("/widgets/bookings-today", async (req, res) => {
if (!secretMatches(req.get("x-dashboardbase-secret"))) {
return res.status(401).json({ error: "unauthorized" })
}
const { rows } = await db.query(`
SELECT
count(*) FILTER (WHERE created_at >= date_trunc('day', now())) AS today,
count(*) FILTER (
WHERE created_at >= date_trunc('day', now()) - interval '7 days'
AND created_at < now() - interval '7 days'
) AS last_week
FROM bookings
`)
const today = Number(rows[0].today)
const lastWeek = Number(rows[0].last_week)
const change = lastWeek > 0 ? Math.round(((today - lastWeek) / lastWeek) * 100) : 0
res.json({
title: "Bookings today",
actions: [{ title: "View Bookings", type: "link", url: "https://example.com/admin/bookings" }],
data: {
header: {
title: String(today),
subtitle: "vs same day last week",
badge: {
text: `${change >= 0 ? "+" : ""}${change}%`,
icon: change >= 0 ? "ArrowUp" : "ArrowDown",
color: change >= 0 ? "Success" : "Danger",
},
},
},
})
})"Same day last week" compares today so far with the same hours a week ago, so the badge doesn't look alarming at 9 a.m. The query often already exists in the client's admin panel; the endpoint just reshapes the result.
dashboardbase sends the workspace secret as an x-dashboardbase-secret header on every request. When you test a datasource, it also makes one request with every credential stripped and tells you whether the endpoint really refused it. That result is worth showing the client's security reviewer.
How to create a personalized client dashboard for each client
- A workspace per client is the clean setup when you build for several clients. Each workspace has its own endpoint secret, branding, TVs and alerts, and the client's team are members who can view the dashboards but not edit them. The client can hold the subscription, so the handover is simple when the engagement ends.
- A shared link is the lightest setup: a password-protected public link at
app.dashboardbase.com/s/…that the client opens in any browser, with no account.
What clients get: numbers that refresh every 1, 5, 10 or 30 minutes; the native iOS and Android apps; their office TV, paired by opening www.dashboardbase.com/tv (how TV pairing works); and alerts by email, push or Slack when an endpoint returns an alert, such as the app going down.
Plans are flat per workspace: Startup $9.99/month (3 seats), Team $29.99 (10 seats), Business $79.99 (25 seats), with a 14-day free trial. You don't pay per dashboard, and shared links don't use seats.
When to build it into the app instead
- The client's customers need the dashboard, inside the product. Customer-facing analytics is part of the product, so build it.
- The client wants a white-label portal on their own domain or scheduled PDF reports. dashboardbase does neither.
- The job is ad, SEO and social reporting rather than the app's own data. Marketing-agency tools fit better; see the comparison.
For everything else (the client, their managers and their office TV watching the numbers of the app you built), a few endpoints beat a dashboard you have to host.
FAQ
What is a client reporting dashboard? A live view, for a client, of the numbers an agency is responsible for. For a development agency, those numbers usually live in the app the agency built.
Do I need to build a frontend for each client's dashboard? No. Each widget is a read-only endpoint in the client's backend that returns JSON; dashboardbase renders it on the web, on phones and on a TV.
Does client data get copied to dashboardbase? No. dashboardbase calls the endpoints on a schedule and renders the result; it doesn't store the data.
How do I create a personalized client dashboard? Give each client their own workspace with their logo, colours and the widgets for their app, and invite their team as members. Each client then sees only their own numbers.
Can clients see the dashboard without an account? Yes, through a password-protected shared link. To use the mobile apps they join the client's workspace as members.