SaaS platform for PDF authenticity verification with a public REST API.
Role-gated admin dashboard for the HTPBE? SaaS platform – real-time KPIs, per-user quota tracking, and a zero-dependency bar chart. Data is fetched server-side in React Server Components.
Libraries
Databases
Services
Key Results

Once HTPBE? went live with paying customers, operating the platform blind became a real problem. I needed to answer everyday business questions: how many users converted to a paid plan this month? Which accounts are close to their quota limit and likely to upgrade? Did a specific user actually use the product, or just sign up and disappear?
The answers existed, but scattered. The payment provider’s dashboard held billing, the raw database held usage, and stitching them together on every question was slow and error-prone. There was no single place to see the health of the business – who is active, who is at risk, who is worth a nudge toward a higher plan.
I built one internal dashboard that combines everything an operator needs on a single screen: total users, subscribers and MRR up top, an active-subscriptions table, and a per-account drill-down showing usage, quota progress, subscription state, and recent activity. No separate backend, no extra data-fetching library – the pages read straight from the database and render as finished HTML.
Three design decisions drove the value. First, the whole admin area is locked to the owner’s account with a server-side check that runs before any data loads – there is no browser-side toggle to bypass. Second, subscription quota usage is read from the same credit balance the billing logic spends from (the monthly quota minus the credits remaining), so a check that costs more than one credit is reflected immediately instead of being undercounted by a row count. Third, each account carries a colour-coded quota bar (yellow as it fills, red near the limit) so accounts approaching their cap – the natural upgrade candidates – surface at a glance. The Mollie customer and subscription IDs sit right on the page, so investigating a payment issue starts without a database lookup.
The result is a single operational cockpit for the SaaS: who converted, who is active, who is near their limit, and how each account is billed – answerable in seconds instead of by cross-referencing tools.
| Metric | Value |
|---|---|
| Client-side data fetching | None – every query runs on the server |
| Chart library | None – plain JSX |
| Subscription quota usage | Monthly quota minus subscriptionCreditsRemaining, not a row count |
| Access control | Single email check, server-side, before any data query executes |
The overview aggregates KPIs across every account – total users, subscribers with their share of users and MRR, accounts on pay-as-you-go credits, and accounts active in the last 30 days – above an active-subscriptions table with plan, status, next charge, scheduled plan change, and price. The main users table is tabbed by recent activity (active within 7 days, cooling at 8–30 days, dormant, never active, all) and shows acquisition channel, spend, check counts, and last-active and registration dates, each row linking to a per-user page.
Each per-user page shows a billing and credits card (plan, status, billing currency, monthly quota, current period, overage, credit balance, and the Mollie customer and subscription IDs) with admin actions to block the account, grant credits, and toggle overage, the account’s API keys with their environment and last-used status, a 30-day activity chart, and a paginated, filterable list of the account’s checks with result and file metadata. Separate admin pages list API logs, how often each modification marker fires, and the welcome-grant appeals queue.
For the technically curious, here is how the core pieces are built.
The admin routes – overview, per-user detail, logs, markers, and appeals – are React Server Components under the existing (dashboard) layout group, for example the overview at app/(dashboard)/dashboard/admin/page.tsx and per-user detail at app/(dashboard)/dashboard/admin/[userId]/page.tsx. The guard runs at the top of each, before any data is fetched:
// app/(dashboard)/dashboard/admin/page.tsx
const session = await auth();
requireAdmin(session);
// lib/auth/admin.ts
export function requireAdmin(session: Session | null): void {
if (!isAdminEmail(session?.user?.email)) {
redirect("/dashboard");
}
}Next.js redirect() in a Server Component throws immediately, so no data queries execute for unauthorized sessions. There is no client-side conditional rendering to bypass, and the admin Server Actions (block, grant credits, overage) re-check the same email on the server.
The overview page reads all users, then counts checks grouped by user and source. Grouping by source in SQL (rather than filtering in JavaScript) means the database does the work once:
// app/(dashboard)/dashboard/admin/page.tsx
const checkAgg = await db
.select({
userId: checks.userId,
source: checks.source,
total: sql<number>`cast(count(*) as integer)`,
})
.from(checks)
.where(sql`${checks.userId} IS NOT NULL`)
.groupBy(checks.userId, checks.source);The result rows are indexed into a Map<userId, { total: number; web: number; api: number }> in JavaScript before rendering – one pass, no repeated array searches per table row. The subscriber count and MRR are derived from the same allUsers array, with no extra query.
Subscription quota usage is derived from the credit pool rather than a check-row count: the monthly quota minus the credits still remaining. A check that costs more than one credit is therefore reflected immediately instead of being undercounted by COUNT(*):
// app/(dashboard)/dashboard/admin/[userId]/page.tsx
const limit = user.requestsPerMonth ?? 0;
const creditsUsed = isActiveSub ? creditsUsedThisPeriod(user) : 0;
const usagePct = isActiveSub && limit ? Math.min(Math.round((creditsUsed / limit) * 100), 100) : 0;
// Progress bar colour: red ≥90%, yellow ≥70%, primary otherwise
const barColor = usagePct >= 90 ? "bg-red-500" : usagePct >= 70 ? "bg-yellow-500" : "bg-primary";
// lib/services/subscription-credit-pool.ts
Math.max(0, row.requestsPerMonth - (row.subscriptionCreditsRemaining ?? 0));Overage (checks beyond the monthly quota) is surfaced separately so I can see accounts that went over before a plan upgrade was processed.
I considered Recharts and Chart.js but rejected both – a single bar chart does not justify a 200+ kB dependency in an internal tool. The chart renders in pure JSX: a flex row of proportionally scaled div elements, each with a CSS height set from the day’s count relative to the busiest day in the 30-day window. Days with zero checks still render (height 0), which keeps the x-axis gaps honest:
// app/(dashboard)/dashboard/admin/[userId]/page.tsx
// Fill all 30 days from epoch-day arithmetic so zero-check days are explicit
const todayDay = Math.floor(Date.now() / 1000 / 86400);
const dailyMap = new Map(dailyRows.map((r) => [r.day, r.count]));
const chartDays: { label: string; count: number }[] = [];
for (let i = 29; i >= 0; i--) {
const day = todayDay - i;
const d = new Date(day * 86400 * 1000);
chartDays.push({
label: d.toLocaleDateString("en-GB", { day: "2-digit", month: "short", timeZone: "UTC" }),
count: dailyMap.get(day) ?? 0,
});
}// Rendered chart — no library, pure JSX
<div className="flex h-32 items-end gap-0.5">
{chartDays.map((day, i) => {
const pct = maxCount > 0 ? (day.count / maxCount) * 100 : 0;
return (
<div
key={i}
className="group relative flex flex-1 flex-col items-center justify-end"
title={`${day.label}: ${day.count}`}
>
<div
className="w-full rounded-t bg-primary/70 transition-all group-hover:bg-primary"
style={{ height: `${Math.max(pct, day.count > 0 ? 3 : 0)}%` }}
/>
{day.count > 0 && (
<div className="pointer-events-none absolute bottom-full hidden whitespace-nowrap rounded bg-black/80 px-2 py-1 text-xs text-white group-hover:block">
{day.label}: {day.count}
</div>
)}
</div>
);
})}
</div>The Math.max(pct, day.count > 0 ? 3 : 0) guard ensures a bar with one check is still visible even when the busiest day is in the hundreds.
The main table lists the users in the selected activity tab with name, email, acquisition channel, billing state, credits, spend, check counts, last-active and registration dates. Each row has a ‘Details →’ link to the per-user page.
The per-user detail page shows:
ChecksTable component already used elsewhere in the dashboard, 50 per page with filters, showing filename, check date, verdict, source, key name with environment badge, how the check was funded, and PDF creator/producer metadataThe Mollie customer and subscription IDs come from the users table and are shown as plain text; the page makes no Mollie API call at render time.
AvailableNeed something similar?
I build custom solutions – from APIs to full products. Let’s talk about your project.
SaaS platform for PDF authenticity verification with a public REST API.
Developer-first SaaS API for EU VAT validation via VIES with Redis caching, change monitoring, and webhook notifications.
40+ point production SaaS checklist: auth, Stripe billing, PostgreSQL, rate limiting, email, monitoring, and security – with honest 8-week build estimates.
Stack
Databases
Turborepo monorepo with Next.js and Hono, sharing one set of TypeScript types across frontend and backend.