Iurii RoguliaIurii Rogulia
AboutAI EngineeringServicesProjectsAI SkillsStackTestimonialsPhrasesBlog
Contact
Iurii ships.

Iurii Rogulia, senior full-stack software engineer. Professionally building software since 2001.

Think of a numberThink of a birthday
Hourly ratesChecklistsPrivacy PolicyCookie Policy

Business

TMI Iurii Rogulia
VAT ID: FI29845875
DUNS: 368664211
Lappeenranta, Finland 🇫🇮

  1. Home
  2. /
  3. Projects
  4. /
  5. HTPBE.TECH – Internal Admin Dashboard

Iurii Oversees: HTPBE.TECH – Internal Admin Dashboard

March 15, 2026

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.

Live Demo

Stack

Next.jsReactTypeScriptNode.jsTailwind CSS

Libraries

Drizzle ORMNextAuth.jspinoZod

Databases

PostgreSQL

Services

Mollie

Topics

Admin DashboardArchitectureAuthSaaS

Key Results

  • One screen to run the business – conversions, revenue signals, and per-account usage without stitching the payment provider and the database by hand
  • Spot at-risk accounts early – live quota progress bars flag users near their limit before they hit it
  • Billing data you can trust – usage read from the same credit balance that billing spends from, so a multi-credit check is counted correctly
  • Locked to the owner alone – a single server-side access check, no way to bypass from the browser
  • Fast, cheap internal tool – zero chart libraries and no client data layer keep it lightweight
HTPBE.TECH – Internal Admin Dashboard

The Business Problem

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.

The Solution

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.

Results

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.

MetricValue
Client-side data fetchingNone – every query runs on the server
Chart libraryNone – plain JSX
Subscription quota usageMonthly quota minus subscriptionCreditsRemaining, not a row count
Access controlSingle email check, server-side, before any data query executes

What the Dashboard Shows

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.

Under the Hood

For the technically curious, here is how the core pieces are built.

Role Guard

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.

Aggregating KPIs in a Single Pass

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.

Per-User Quota Progress

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.

30-Day Activity Chart in Pure JSX

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.

Users Table and Detail Drill-Down

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:

  • A header with the plan badge and the Mollie customer ID, plus controls to block the account
  • A billing and credits card with plan, status, billing currency, quota per month, current period dates, overage count and opt-in, credit balance, the Mollie subscription ID, and a list of purchases and grants
  • The API keys list: environment badge (live = green, test = yellow), the last four characters of the key, creation date, last-used date, and active/inactive status
  • The 30-day bar chart described above
  • The account’s checks via the shared 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 metadata

The 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.

Iurii RoguliaAvailable

Need something similar?

I build custom solutions – from APIs to full products. Let’s talk about your project.

View all projects

Related projects

HTPBE.TECH – Has This PDF Been Edited?
HTPBE.TECH – Has This PDF Been Edited?
September 25, 2024
HTPBE.TECH – Has This PDF Been Edited?

SaaS platform for PDF authenticity verification with a public REST API.

Stack

Next.jsReactTypeScriptNode.jsTailwind CSSNixpacks

Databases

PostgreSQLRedis
vatnode.dev – EU VAT Validation API
vatnode.dev – EU VAT Validation API
January 19, 2026
vatnode.dev – EU VAT Validation API

Developer-first SaaS API for EU VAT validation via VIES with Redis caching, change monitoring, and webhook notifications.

Stack

Next.jsReactHonoTypeScriptNode.jsTailwind CSSTurborepoDocker

Databases

PostgreSQLRedis

Related posts

Next.js SaaS Checklist: Launch Production-Ready in 8 Weeks
January 19, 2026· 17 min
Next.js SaaS Checklist: Launch Production-Ready in 8 Weeks

40+ point production SaaS checklist: auth, Stripe billing, PostgreSQL, rate limiting, email, monitoring, and security – with honest 8-week build estimates.

Stack

Next.jsTypeScript

Databases

PostgreSQLRedis
Turborepo Monorepo: Next.js and Hono in One Repo With Shared Types
August 4, 2025· 13 min
Turborepo Monorepo: Next.js and Hono in One Repo With Shared Types

Turborepo monorepo with Next.js and Hono, sharing one set of TypeScript types across frontend and backend.

Stack

Next.jsTypeScriptHonoTurborepoNode.jsDocker

Databases

PostgreSQLRedis