SaaS10 min

Building a SaaS admin portal that doesn't embarrass you

A production playbook for SaaS admin tooling — safe impersonation, immutable audit logs, user search, refunds, and feature-flag toggles without handing support a loaded gun.

The admin portal is the tool support uses when a customer says nothing works. It's also the tool an attacker wants most, because one account can change everything. Early-stage teams ship admin features as quick React routes behind an is_admin flag and a Retool dashboard that hits the production database directly. That gets through the first hundred customers. Around customer number five hundred — usually when the first SOC 2 auditor asks to see impersonation logs — the cracks show. This piece covers the pattern we ship on client projects: safe impersonation, scoped permissions, immutable audit logs, and an action surface that support teams can actually use without needing a Slack channel with engineering.

The surface: what an admin portal actually needs

Start from the operations that support and ops actually perform on a given week. The 80% case is remarkably consistent across SaaS products: find a user, look at their account, check their recent activity, take an action, and log out. Build for that loop and resist the urge to surface every table in the database.

Admin actionRequired inputsAudit fields captured
User / org searchEmail, user ID, org ID, or Stripe customer IDactor, query string, hit count, timestamp
Impersonate userTarget user ID, reason, ticket referenceactor, target, reason, ticket, session start/end, IP
Resend invite / password resetTarget user IDactor, target, action, timestamp
Toggle feature flag for accountAccount ID, flag key, valueactor, target, flag, old value, new value, reason
Issue refundInvoice or charge ID, amount, reasonactor, target, amount, reason, Stripe refund ID
Extend trial / credit accountAccount ID, days or amount, reasonactor, target, delta, reason, new period end
Suspend / unsuspend accountAccount ID, reason, ticket referenceactor, target, previous state, new state, reason
Export account dataAccount ID, export scopeactor, target, scope, file hash, delivery channel

Every action in the portal is one row in an audit log. If a feature can't articulate what its audit record looks like, it isn't ready to ship. The audit schema is the spec.

Impersonation done right

Impersonation is the feature that makes support fast and the feature that ends careers when it's done badly. Three properties make it safe: every session is explicitly initiated with a reason and a time box, the impersonated session carries a visible banner both to the admin and to anyone watching the audit stream, and the impersonator cannot masquerade in a way that forges the real user's identity for irreversible actions.

// Impersonation middleware — Express-style pseudocode
export async function requireImpersonation(req, res, next) {
  const session = await loadSession(req);
  if (!session) return res.status(401).end();

  if (session.impersonating) {
    // Block sensitive actions entirely while impersonating.
    if (isSensitiveRoute(req.path)) {
      return res.status(403).json({
        error: "impersonation_blocked",
        message: "This action is disabled during impersonation sessions.",
      });
    }

    // Every request during impersonation writes an audit row.
    await auditLog.write({
      actor: session.adminUserId,
      onBehalfOf: session.impersonatedUserId,
      action: req.method + " " + req.path,
      ip: req.ip,
      userAgent: req.headers["user-agent"],
      ticketRef: session.impersonationTicketRef,
      reason: session.impersonationReason,
      sessionId: session.id,
    });

    // Hard expiry — sessions self-terminate after 30 minutes.
    if (Date.now() > session.impersonationExpiresAt) {
      await endImpersonation(session);
      return res.status(440).json({ error: "impersonation_expired" });
    }
  }

  req.effectiveUser = session.impersonating
    ? await loadUser(session.impersonatedUserId)
    : await loadUser(session.adminUserId);

  req.admin = session.impersonating
    ? await loadUser(session.adminUserId)
    : null;

  next();
}
  • Require a written reason and a ticket reference to start a session. This is the single best deterrent to casual snooping.
  • Time-box every session. 30 minutes is the sweet spot — long enough for any real investigation, short enough to contain blast radius.
  • Block irreversible, high-risk actions during impersonation: password changes, 2FA resets, email changes, API key generation, data deletion. These route to a separate operator-authenticated flow.
  • Render a persistent banner in the impersonated UI. If the impersonated user has a live session concurrently, show them a notice too — this is a trust signal, not a leak.
  • Never impersonate without the target account's real credentials — the session is a server-side shim that marks requests, not a login bypass that reads their password.

The subtle leak: logging impersonated actions only under the admin's ID. Reports, exports, and emails must carry both the impersonator and the impersonated identity, or else a support agent's activity hides inside customer activity dashboards.

Audit logs that survive contact with auditors

An audit log is only useful if it's immutable, queryable, and complete. The three failure modes are: a log table that admins can DELETE from, a log stream that drops events on backpressure, and a schema too loose to answer an auditor's question. Plan for all three.

Schema and storage

  • Write audit events to an append-only store. Either a dedicated table with an insert-only role and no DELETE grant, or a managed service (CloudWatch with object-lock, a SIEM, or an audit-log SaaS).
  • Required fields on every row: actor ID, actor type (user, admin, system, api_key), target resource (type and ID), action verb, timestamp (NTP-synced server time), source IP, user agent, session ID, and a free-form metadata blob.
  • Reserve one column for a human-readable summary. It costs nothing at write time and saves hours when a support lead is scanning the last 48 hours.
  • Retain audit data on a schedule. SOC 2 and ISO 27001 typically want at least one year; some customer contracts want seven. Put this in config, not code.

What to log, what to skip

Over-logging is almost as bad as under-logging. A noisy audit stream trains operators to ignore it, which is the same as not having one. The rule that scales: log every action that changes state, every sensitive read (PII exports, billing records, impersonation), and every permission decision that denies access. Skip ordinary reads.

Permission boundaries and role design

Admin roles collapse to three tiers for most SaaS teams and it's worth resisting the urge to multiply them. Support reads and performs bounded actions. Operations adjusts billing and accounts. Engineering can touch flags and infrastructure. A single super-admin role should exist for exactly two people and its every session should page a security channel.

  • Support tier: search, view, impersonate (with reason), resend invites, reset passwords. No billing, no deletes, no flag toggles.
  • Operations tier: everything support has, plus refunds, credits, plan changes, and trial extensions — gated by spend thresholds.
  • Engineering tier: everything operations has, plus per-account feature flags, data exports, and maintenance operations.
  • Super admin: role changes, API key issuance, deletion, and PII exports over a threshold. Paged on every action.

Put spend thresholds on refund and credit actions that escalate to a second admin for approval. Most support tools skip this and then discover it in a post-mortem. A $50 threshold with a two-person rule above it costs nothing to implement and removes an entire class of insider-risk scenarios.

Build vs. buy: Retool and the stitched portal

Retool, Internal, and Forest Admin are fine for the first six months. They stop being fine when support actions need auditability, when impersonation needs to flow through the product's real auth stack, or when compliance asks where the audit trail lives. The realistic trajectory: start on Retool for CRUD over customer records, move impersonation and state-changing actions behind purpose-built admin endpoints in the main app as soon as the first enterprise contract lands. The hybrid stays cheap and the risky surface is owned.

Observability and alerting

The audit log feeds two systems: a searchable archive for investigations and a real-time alerting pipeline for anomalies. The second is what catches insider events before a customer does.

  • Impersonation sessions opened outside business hours page the on-call security engineer.
  • Refunds above a configured threshold notify a finance channel and require a ticket link.
  • Bulk data exports trigger an alert and a post-hoc review.
  • More than N admin actions in M minutes by a single actor triggers a review. This catches both mistaken bulk operations and account takeover.
  • Any failed permission check on an admin endpoint is a page-worthy event — this is cheap and catches privilege-escalation probes immediately.

Key takeaways

  • Build the admin portal around the 80% support loop: find, view, act, log out. Resist the urge to surface every table.
  • Impersonation is safe only with time boxes, reason capture, blocked sensitive actions, visible banners, and dual-identity audit logging.
  • Audit logs are append-only, schema-disciplined, and retained on a policy. No DELETE grant on the audit table, ever.
  • Three admin tiers — support, operations, engineering — plus a tightly-watched super admin cover most SaaS needs. Spend thresholds and two-person rules remove insider-risk surface.
  • Retool works early. Move impersonation and state-changing actions into first-party admin routes as soon as enterprise contracts arrive.
  • Alert on anomalies, not just on events. The audit log's real job is feeding the detection pipeline, not sitting in a tab nobody opens.
#admin-portal#impersonation#audit-log#saas#support-tools#rbac#compliance
Working on something similar?

Let's build it together.

We ship production SaaS, marketplaces, and web apps. If you want an engineering partner — not a consultancy — let's talk.