Guides

Building Internal Tools & Admin Panels with open-source t...

This guide outlines the architectural steps to build a production-ready internal dashboard using a headless framework like Refine or React Admin. It focuses on multi-source data integration, centralized Role-Based Access Control (RBAC), and audit logging to ensure the tool remains maintainable and secure as the organization scales.

4-6 hours6 steps
1

Configure Multi-Source Data Providers

Internal tools rarely rely on a single database. Configure a primary data provider for your main application DB and secondary providers for external APIs (e.g., Stripe, Zendesk). This allows components to fetch data from disparate sources using a unified hook interface.

dataProvider.ts
import dataProvider from '@refinedev/simple-rest';

const API_URL = 'https://api.internal.com';
const STRIPE_URL = 'https://api.stripe.com/v1';

export const providers = {
  default: dataProvider(API_URL),
  stripe: dataProvider(STRIPE_URL),
};

⚠ Common Pitfalls

  • Mixing authentication headers between providers
  • Failing to normalize data structures from different APIs before they reach the UI components
2

Implement Centralized RBAC Logic

Instead of checking roles inside every component, implement an Access Control Provider. This centralizes permission logic, allowing you to define 'canAccess' rules based on resource, action (list, create, edit, delete), and user role.

accessControl.ts
const permissions = {
  admin: ['users', 'settings', 'billing'],
  support: ['users'],
};

export const accessControlProvider = {
  can: async ({ resource, action }) => {
    const role = await getRole();
    if (role === 'admin') return { can: true };
    if (permissions[role]?.includes(resource)) return { can: true };
    return { can: false, reason: 'Unauthorized' };
  },
};

⚠ Common Pitfalls

  • Relying solely on UI-level role checks without corresponding backend enforcement
  • Hardcoding roles directly in JSX which makes permission updates difficult
3

Build High-Performance Data Tables

Internal users require dense data views. Implement server-side pagination, sorting, and filtering by default. Use a headless table hook to manage state while rendering with a performant UI library like shadcn/ui or MUI.

UserList.tsx
import { useTable } from '@refinedev/core';

const { tableProps } = useTable({
  resource: 'users',
  pagination: { mode: 'server', current: 1, pageSize: 20 },
  filters: { mode: 'server' },
  sorters: { mode: 'server' }
});

⚠ Common Pitfalls

  • Client-side filtering on datasets exceeding 1000 rows causing UI lag
  • Not debouncing search inputs, leading to excessive API requests
4

Integrate Audit Logging Hooks

For compliance and debugging, track all write operations. Use a global hook or interceptor in your data provider to log the user ID, timestamp, resource changed, and the previous/new state of the data.

auditLog.ts
const auditLogProvider = {
  create: async ({ resource, action, data, author }) => {
    await fetch('/api/logs', {
      method: 'POST',
      body: JSON.stringify({ resource, action, data, author, timestamp: new Date() }),
    });
  },
};

⚠ Common Pitfalls

  • Logging sensitive PII (Passwords, SSNs) into the audit log
  • Synchronous logging that blocks the main UI thread during data submission
5

Standardize Form Validation and Error Handling

Use a schema validation library (e.g., Zod) to ensure data integrity before submission. Map backend validation errors (422 Unprocessable Entity) back to specific form fields to improve user experience.

UserSchema.ts
import { z } from 'zod';

export const userSchema = z.object({
  email: z.string().email(),
  role: z.enum(['admin', 'editor', 'viewer']),
  status: z.boolean(),
});

⚠ Common Pitfalls

  • Mismatch between frontend validation and database constraints
  • Generic 'Error occurred' toasts that don't help the user fix specific field inputs
6

Environment-Specific Configuration

Internal tools often perform destructive actions. Configure separate environments (Staging/Production) and use distinct visual cues (e.g., a red header bar in Production) to prevent accidental data modification in the wrong environment.

⚠ Common Pitfalls

  • Connecting a local development build to a production database
  • Hardcoding API keys instead of using environment variables

What you built

By following this structured approach, you ensure that your internal tool is not just a collection of forms, but a secure, auditable platform. Prioritizing RBAC and multi-source integration early prevents the common 'rebuild vs buy' dilemma as organizational requirements become more complex.