Explore
Building Internal Admin Dashboards That Actually Work

Building Internal Admin Dashboards That Actually Work

Most admin dashboards start as a quick internal tool and become critical operational infrastructure. Here's how to build them right from the start.

Building Internal Admin Dashboards That Actually Work

Every SaaS product eventually needs an admin dashboard. Customer support needs to look up accounts. Operations needs to process exceptions. Finance needs to pull billing data. Founders need to see what's happening.

Admin dashboards often start as quick, rough tools: a few SQL queries, a Retool instance, or a hastily built page that wasn't designed to last. Then they become critical infrastructure that everyone depends on and no one wants to touch.

Here's how to build them correctly.


What admin dashboards need

The core requirements for a functional admin dashboard:

User lookup: Find a user by email, name, or ID. See their account details, subscription status, recent activity, and any flags on their account.

Account management: Reset passwords, change subscription plans, apply credits, merge accounts, impersonate users for debugging.

Data tables: Search, filter, sort, and paginate through records. Export to CSV for analysis.

Audit log: Every admin action should be logged with the admin's identity, timestamp, and what changed. This is essential for compliance and debugging.

Access control: Not every support person needs every admin action. Tier your admin permissions — basic lookup for support, account modification for senior support, data access for operations, full access for engineering.


The data table component

The most-used component in any admin dashboard. It needs to be reusable and powerful:

<!-- components/admin/DataTable.vue -->
<script setup generic="T extends Record<string, any>">
interface Column<T> {
  key: keyof T
  label: string
  sortable?: boolean
  render?: (value: T[keyof T], row: T) => string
}

const props = defineProps<{
  columns: Column<T>[]
  data: T[]
  loading?: boolean
  total?: number
  page?: number
  perPage?: number
}>()

const emit = defineEmits<{
  sort: [key: string, direction: 'asc' | 'desc']
  pageChange: [page: number]
  rowClick: [row: T]
}>()

const sortKey = ref<string | null>(null)
const sortDir = ref<'asc' | 'desc'>('asc')

function handleSort(key: string) {
  if (sortKey.value === key) {
    sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
  } else {
    sortKey.value = key
    sortDir.value = 'asc'
  }
  emit('sort', key, sortDir.value)
}
</script>

The key design decision: keep sorting, pagination, and filtering server-side. Client-side sorting only works for small datasets. Admin tables often need to search hundreds of thousands of records.


Search and filtering

Admin search needs to be fast and flexible:

// server/api/admin/users.get.ts
export default defineEventHandler(async (event) => {
  requireAdminAuth(event)

  const query = getQuery(event)
  const {
    search,
    plan,
    status,
    sortBy = 'created_at',
    sortDir = 'desc',
    page = 1,
    perPage = 50
  } = query

  let dbQuery = supabase
    .from('users')
    .select('*, workspaces(name, plan)', { count: 'exact' })

  if (search) {
    dbQuery = dbQuery.or(
      `email.ilike.%${search}%,name.ilike.%${search}%`
    )
  }

  if (plan) {
    dbQuery = dbQuery.eq('workspaces.plan', plan)
  }

  if (status) {
    dbQuery = dbQuery.eq('status', status)
  }

  const { data, count } = await dbQuery
    .order(sortBy as string, { ascending: sortDir === 'asc' })
    .range((page - 1) * perPage, page * perPage - 1)

  return { users: data, total: count }
})

User impersonation

One of the most useful admin capabilities: impersonating a user to see exactly what they see. Essential for debugging "I can't get the checkout to work" support tickets.

Implementation with Supabase:

// server/api/admin/impersonate.post.ts
export default defineEventHandler(async (event) => {
  const admin = requireAdminAuth(event)
  const { userId } = await readBody(event)

  // Log the impersonation
  await auditLog.create({
    adminId: admin.id,
    action: 'impersonate_user',
    targetUserId: userId,
    timestamp: new Date()
  })

  // Create a short-lived impersonation token
  // Store it with the admin's identity so it can be detected and reverted
  const token = createImpersonationToken(admin.id, userId)

  return { token, redirectUrl: '/app' }
})

Crucially: impersonation sessions should be visually distinct in the UI (a banner: "Viewing as user name — Return to admin") and logged in the audit trail.


The audit log

Every action in an admin dashboard changes data. Track it all:

// server/utils/audit.ts
export async function auditLog(params: {
  adminId: string
  action: string
  targetType: string
  targetId: string
  before?: Record<string, any>
  after?: Record<string, any>
}) {
  await db.adminAuditLog.create({
    ...params,
    timestamp: new Date(),
    ipAddress: getRequestIP(event),
    userAgent: getRequestHeader(event, 'user-agent')
  })
}

Admin audit logs are often required for SOC 2 compliance and are invaluable when a customer asks "what happened to my account?"


Access control

Not all admin users need all admin capabilities:

// server/middleware/admin-auth.ts
const ADMIN_PERMISSIONS = {
  'support': ['view_users', 'view_accounts', 'reset_password'],
  'senior_support': ['view_users', 'view_accounts', 'reset_password', 'modify_subscription'],
  'operations': ['view_users', 'view_accounts', 'modify_subscription', 'apply_credits', 'export_data'],
  'engineer': ['*'] // All permissions
}

export function requireAdminPermission(event: H3Event, permission: string) {
  const admin = event.context.admin
  const permissions = ADMIN_PERMISSIONS[admin.role] ?? []

  if (!permissions.includes('*') && !permissions.includes(permission)) {
    throw createError({ statusCode: 403, message: 'Insufficient permissions' })
  }
}

When to use a third-party tool

Building a custom admin dashboard makes sense when:

  • You need deep integration with your data model
  • You have specific workflows that generic tools don't support
  • You have a lot of users and need sophisticated search and filtering

Consider Retool, AppSmith, or Internal.io when:

  • You need something working within days, not weeks
  • Your admin requirements are standard (CRUD on your tables)
  • You don't have the capacity to build and maintain custom admin tooling

The custom vs. third-party decision is about timeline vs. fit. Both are legitimate choices.

Need a custom admin dashboard built fast? Let's talk →