Guides

Building Admin panel builder comparison with Retool and A...

This guide provides a structured approach to building internal tools with a focus on balancing custom development and low-code solutions. Key considerations include data integration, access control, and maintainability for startup-scale applications.

3-5 hours6 steps
1

Define data model and API requirements

Map internal tool workflows to database schemas. Identify required APIs (internal/external) and data transformations. Use Prisma schema or Supabase database editor to create models.

prisma/schema.prisma
model User {
  id      Int    @id @default(autoincrement())
  name    String
  role    Role   @relation(fields: [roleId], references: [id])
  roleId  Int
}

enum Role {
  ADMIN
  EDITOR
  VIEWER
}
2

Set up project structure

Create Next.js app with API routes. Configure authentication middleware. Initialize database client (Prisma or Supabase). Use shadcn/ui for component baseline.

setup.sh
npx create-next-app@latest internal-tools --typescript
npm install @prisma/client shadcn/ui
3

Implement role-based access control

Create middleware to validate user roles against requested routes. Store permissions in database. Use React context for client-side access checks.

middleware.ts
const middleware = (auth: AuthClient) => {
  return async (req: NextRequest) => {
    const user = await auth.getUser();
    if (!user.roles.includes('ADMIN')) {
      return NextResponse.redirect(new URL('/unauthorized', req.url));
    }
    return NextResponse.next();
  };
};

⚠ Common Pitfalls

  • Missing server-side validation for critical operations
  • Inconsistent role state between client and server
4

Build data table components

Create reusable table components with sorting, filtering, and inline editing. Use server-side pagination for large datasets. Implement column configuration via JSON schema.

DataGrid.tsx
const DataGrid = ({ columns, fetchData }) => {
  const [sort, setSort] = useState({ field: '', direction: 'asc' });
  const data = useQuery(['data', sort], () => fetchData(sort));
  
  return (
    <table>
      <thead>
        {columns.map(col => (
          <th key={col.key} onClick={() => setSort({ field: col.key, direction: ... })}>
            {col.label}
          </th>
        ))}
      </thead>
      <tbody>{data.map(row => <tr>{columns.map(col => <td>{row[col.key]}</td>)}</tr>)}</tbody>
    </table>
  );
};
5

Connect multiple data sources

Create API proxies for external services. Use unified data transformation layer. Implement error handling for API failures and rate limiting.

api/proxy.ts
const fetchExternalData = async (endpoint: string) => {
  try {
    const response = await fetch(`https://api.example.com/${endpoint}`, {
      headers: { Authorization: `Bearer ${process.env.EXTERNAL_API_KEY}` },
    });
    return await response.json();
  } catch (error) {
    logError('External API failure', error);
    throw new Error('Data unavailable');
  }
};

⚠ Common Pitfalls

  • Hardcoded API keys in client code
  • Insufficient retry strategies for transient failures
6

Add audit logging

Track user actions with timestamps and IP addresses. Store logs in separate database table. Implement log retention policy.

audit.ts
const logAction = async (userId: number, action: string, details: any) => {
  await prisma.auditLog.create({
    data: {
      userId,
      action,
      ip: getIPAddress(),
      timestamp: new Date(),
      details: JSON.stringify(details)
    }
  });
};

What you built

This implementation sequence addresses core internal tool requirements while maintaining security and scalability. Validate against actual workflow needs and adjust data modeling based on API response patterns and access frequency.