Your support inbox has a ticket you can't answer without opening Stripe, your database console and a log viewer, and you're the only person who knows where each one lives. Before you build anything, list the five costliest support questions from the last month and write down which system holds the answer to each. That list is your admin dashboard spec.
Most founders build an admin panel reactively, one request at a time, and it grows faster than the product. This post sets out what a first-year SaaS admin dashboard needs, how to handle impersonation without opening a security hole, and where to stop. It draws on the multi-tenant patterns in the Callidus case study, a React, Firebase and Stripe Connect clinic platform, and on the primary documentation for the parts that change.
What does a SaaS admin dashboard need in its first year?
A year-one SaaS admin dashboard needs five things: customer lookup, a subscription state inspector, tenant settings override, manual billing actions and audit log search. Everything else can wait until a real ticket asks for it.
You don't need all five on day one. Build first whatever removes your longest support investigations, which for most products means lookup plus the subscription inspector. The table is my suggested order, based on what usually triggers each need. It's a recommendation, not a measurement.
| Feature | What triggers building it | What happens without it |
|---|---|---|
| Customer lookup | First ticket you can't reproduce | You guess at account state |
| Subscription state inspector | First "I paid but it says trial expired" message | You cross-check Stripe and your database by hand |
| Tenant settings override | First customer who needs a change they can't self-serve | Someone edits the database directly |
| Manual billing actions | First refund or trial extension | It happens in the Stripe dashboard with no record in your app |
| Audit log search | First question about who changed what | Nobody can answer it |
If the terms are new, the glossary entries on B2B SaaS and multi-tenant SaaS define them.
What should a subscription state inspector show?
It should put the customer's plan, Stripe subscription status, billing period, overrides and latest billing event on one screen, so a billing question doesn't need three browser tabs.
Stripe's subscription documentation defines eight statuses: trialing, active, incomplete, incomplete_expired, past_due, canceled, unpaid and paused. Show the raw status next to your own plain-language label, because past_due and unpaid lead to different fixes. Then add:
- current period start and end
- the plan name and the internal Stripe price ID, since the marketing label alone hides which price is billing
- manual overrides and which admin applied them
- the last webhook event you processed, with its type and timestamp
- open invoices and their payment state
That last field needs work on your side. Stripe's webhook documentation says events aren't guaranteed to arrive in the order they were generated, can be delivered more than once, and are retried for up to three days in live mode. Store the event ID and type you processed and deduplicate on the ID, or the inspector will show a confident but wrong "last event".
Callidus handles twenty Stripe webhook events end to end according to its case study, including invoice.payment_action_required, which starts a 3D Secure escalation. An admin view that shows what the handler last did with an event is the natural companion to that plumbing. In a booking product such as BookBed, the equivalent screen would show booking status, payment state and payout state together. That's a design suggestion, not a description of a shipped BookBed feature.
How should manual billing actions work?
Route every manual billing action through the Stripe API, record it in your audit log, and let the resulting webhook update your own state, so your database never disagrees with Stripe about what happened.
That's my recommendation, and it follows from the webhook behaviour above. If an admin button writes "trial extended" straight into your database while Stripe still has the old trial end, the next webhook overwrites it or the two records drift apart. Make the button call Stripe, write the audit entry with the admin's ID and the before and after values, and show the change in the inspector when the event arrives. The same rule covers refunds and plan changes.
Is customer impersonation safe to build?
Impersonation is safe only when it creates its own short-lived session, shows a persistent banner naming the agent, and records every change with the agent's identity. Without those three, it's a shared password with no log.
The risk is documented. Authress, an authentication vendor, reviewed the dangers of user impersonation on a page last updated in March 2026. Its author reports that in searching for existing implementations, 86% of the repositories and links found no longer exist, were archived over five years ago, or had fewer than ten GitHub stars. The methodology isn't stated, so read that as one practitioner's informal search, not a measured rate. My reading is that most teams build this from scratch, which is a reason to design it carefully. Authress also lists audit-trail corruption among the core risks, because actions taken by an impersonating admin get attributed to the end user.
This is the pattern I'd follow. These are my recommendations, not a standard:
- Require a stated reason and record who started the session and when.
- Show a banner that can't be dismissed for as long as the session lasts.
- Put a hard maximum duration on the session and enforce it in your own code.
- Write the agent's ID next to every mutation, for example an
impersonatedByfield on the change record. - Restrict who can start a session to a short list of trusted roles.
On Firebase, the Admin SDK's createCustomToken accepts a uid and optional additional claims, and the docs state that custom tokens expire after one hour. That limits the token, not necessarily the session it starts, so keep the duration check in your own code. Custom claims also have a 1000-byte payload limit, and the docs say they're meant for access control, not for storing extra data. Keep the claim to an ID and put the reason in the audit log:
const token = await admin.auth().createCustomToken(customerUid, {
impersonatedBy: agentUid, // the reason lives in the audit log
});
Claims also reach a client only when its ID token refreshes, so don't rely on a claim change taking effect instantly.
Session recordings are the lower-risk alternative when you only need to see what a customer saw, and Authress lists them among its recommended alternatives. They don't help when the customer needs you to change a setting for them. For that case, the safest version of "fix this for me" is often a dedicated admin action that edits one field and logs it, with no session swap at all.
What should the audit log record?
An admin audit log should record authentication events, access-control failures, admin actions, privilege changes and access to sensitive data, and it should be tamper-evident. That's the baseline in OWASP's logging cheat sheet.
OWASP says to always log authentication successes and failures, authorization failures, user administration such as privilege changes, use of administrative privileges, and access to sensitive data. On integrity, it advises building in tamper detection so you know if a record was modified or deleted, and copying log data to read-only media as soon as possible. It also warns against logging session IDs, access tokens, passwords and card data, and suggests masking, hashing or encrypting sensitive values instead.
Map that onto an admin panel and the must-log list is short: every login and failed login, every role change, every impersonation start and end, every manual billing action, every settings override and every data export. If a write button exists, it writes a log entry.
Retention is a product decision. According to its case study, Callidus uses three tiers: 90 days for routine events, one year for financial events and two years for GDPR-sensitive events, with PATIENT_DATA_ERASED records never expiring. Whether those numbers suit your product depends on the regulations your customers work under, so check them against the rules that apply to you instead of copying these.
How do you stop the admin panel becoming a second product?
Give it a written scope: it exists to unblock support and diagnose billing, and every proposed feature has to name the ticket category it removes. Anything else belongs in your analytics tool.
Three constraints do most of the work:
- Read-first by default. Add write actions only where a real support pattern justifies them.
- Audit every mutation. No "it's internal" exceptions.
- Define roles before features, and enforce them on the server. Callidus has six roles (super admin, owner, admin, manager, practitioner and receptionist) and gates access through shared helpers such as
hasAdminAccessinstead of inline checks, per its case study. The idea carries to any product: one place decides who can do what, so changing the matrix doesn't mean hunting through screens.
For the infrastructure side, the SaaS MVP stack covers the usual auth and storage choices, and the pillar on SaaS backend infrastructure places admin tooling next to jobs, webhooks and email.
Take the list of five support questions from the start of this post and pick the one that eats the most time. Could a single inspector screen have answered it in under a minute?
Part of the SaaS Backend Infrastructure: Jobs, Webhooks, Email, and Admin Tooling guide.



