Serverpod Admin โ€“ The Missing Admin Panel for Serverpod ๐ŸŽฏ

Finally, an admin panel that Serverpod deserves!

You've built an amazing Serverpod app with powerful endpoints, robust models, and a beautiful frontend. But when it comes to managing your data, you're stuck writing custom endpoints, building one-off admin pages, or worseโ€”directly accessing the database.

That's where Serverpod Admin comes in. This is the missing piece that transforms your Serverpod backend into a fully manageable system.


๐Ÿ  Admin Dashboard Overview

Secure login screen for admin users. Only users with the serverpod.admin scope can access the dashboard.

Admin Login

Browse and manage all your data with a beautiful, intuitive interface.

Admin Dashboard

New record with a clean, user-friendly interface. Admin Dashboard

Powerful search and filtering capabilities to find exactly what you need.

Search & Filter

Edit record with a clean, user-friendly interface.

Edit Records

Delete record with a clean, user-friendly interface. Admin Dashboard

View detailed information about any record.

Record Details

Beautiful empty states when no records are found.

Empty State


โœจ Why Serverpod Admin Matters

๐Ÿš€ Zero Configuration, Maximum Power

No more writing boilerplate CRUD endpoints. Register your models once, and instantly get a complete admin interface with:

  • Browse & Search โ€“ Navigate through all your data with powerful filtering
  • Create & Edit โ€“ Intuitive forms for managing records
  • Delete โ€“ Safe deletion with proper validation
  • Pagination โ€“ Handle large datasets effortlessly

๐ŸŽจ Frontend-Agnostic Architecture

Built with flexibility in mind. The serverpod_admin_server exposes a clean API that any frontend can consume. Start with Flutter today, switch to Jaspr tomorrow, or build your own custom admin UIโ€”the choice is yours!

โšก Built for Serverpod Developers

  • Type-Safe โ€“ Leverages Serverpod's generated protocol classes
  • Integrated โ€“ Works seamlessly with your existing Serverpod setup
  • Extensible โ€“ Designed to grow with your needs

๐Ÿ› ๏ธ Developer Experience First

Stop spending days building admin interfaces. Get back to building features that matter. With Serverpod Admin, you can have a production-ready admin panel in minutes, not weeks.


๐Ÿ“ฆ Installation

Server Side

Run:

flutter pub get serverpod_admin_server

Flutter (Frontend)

Run:

flutter pub get serverpod_admin_dashboard

That's it! You're good to go! ๐Ÿš€


๐Ÿงฉ Quick Start

Registering Models (Server Side)

import 'package:serverpod_admin_server/serverpod_admin_server.dart' as admin;

import 'package:use_serverpod_admin_server/src/generated/protocol.dart';

void registerAdminModule() {
  admin.configureAdminModule((registry) {
    registry.register<Post>();
    registry.register<Person>();
    registry.register<Comment>();
    registry.register<Setting>();
    // Add any model you want to manage!
  });
}

Call registerAdminModule() in your server.dart file just after pod.start()

Setting Up Authentication

Serverpod Admin uses serverpod_auth_idp for authentication. Make sure you have it configured in your server:

pod.initializeAuthServices(
  tokenManagerBuilders: [
    JwtConfigFromPasswords(),
  ],
  identityProviderBuilders: [
    EmailIdpConfigFromPasswords(
      sendRegistrationVerificationCode: _sendRegistrationCode,
      sendPasswordResetVerificationCode: _sendPasswordResetCode,
    ),
  ],
);

Creating an Admin User

To access the admin panel, users must have the serverpod.admin scope. Here's how to create an admin user:

import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_idp_server/core.dart';
import 'package:serverpod_auth_idp_server/providers/email.dart';

Future<void> findOrCreateAndLinkEmail() async {
  // Create a manual session for internal work
  var session = await Serverpod.instance.createSession();

  // Use a nullable ID or UuidValue to track the target user
  UuidValue? authUserId;

  try {
    final emailAdmin = AuthServices.instance.emailIdp.admin;
    const email = 'admin@example.com';
    const password = 'your-secure-password';

    // 1. Check if the email account already exists
    final emailAccount = await emailAdmin.findAccount(
      session,
      email: email,
    );

    if (emailAccount == null) {
      // 2. Create a new AuthUser if no account exists
      final authUser = await AuthServices.instance.authUsers.create(session);
      authUserId = authUser.id;

      // 3. Create the email authentication for the new user
      await emailAdmin.createEmailAuthentication(
        session,
        authUserId: authUserId,
        email: email,
        password: password,
      );
    } else {
      // If account exists, get the ID from the existing record
      authUserId = emailAccount.authUserId;
    }

    // 4. Update the user to have admin scopes using the identified ID
    await AuthServices.instance.authUsers.update(
      session,
      authUserId: authUserId,
      scopes: {Scope.admin},
    );

    print("User $email updated to admin successfully.");
  } catch (e) {
    print("Error creating internal admin: $e");
  } finally {
    // IMPORTANT: Always close manual sessions to prevent memory leaks
    await session.close();
  }
}

Call findOrCreateAndLinkEmail() in your server.dart file after pod.start() to create your first admin user.

Using the Admin Dashboard (Flutter)

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  client = Client('http://localhost:8080/')
    ..connectivityMonitor = FlutterConnectivityMonitor()
    ..authSessionManager = FlutterAuthSessionManager();
  client.auth.initialize();
  runApp(AdminDashboard(client: client));
}

That's it! You now have a fully working admin panel with authentication for your Serverpod app! ๐Ÿš€๐ŸŽ‰

The admin dashboard will automatically show a login screen for unauthenticated users. Only users with the serverpod.admin scope can access the admin panel.


๐ŸŽจ Customization

Serverpod Admin offers flexible customization options, from simple sidebar tweaks to complete UI replacement.

Customize individual sidebar items with custom labels and icons:

AdminDashboard(
  client: client,
  sidebarItemCustomizations: {
    'posts': SidebarItemCustomization(
      label: 'Posts',
      icon: Icons.post_add,
    ),
    'persons': SidebarItemCustomization(
      label: 'Person',
      icon: Icons.person,
    ),
    'comments': SidebarItemCustomization(
      label: 'Comment',
      icon: Icons.comment,
    ),
    'settings': SidebarItemCustomization(
      label: 'Setting',
      icon: Icons.settings,
    ),
  },
)

This allows you to:

  • Customize labels โ€“ Change the display name for any resource
  • Customize icons โ€“ Use your own icons for better visual identification
  • Keep it simple โ€“ Only customize what you need, leave the rest default

Full Customization

For complete control over the admin interface, you can replace any component with your own custom widgets:

AdminDashboard(
  client: client,
  // Custom sidebar - completely replace the default sidebar
  customSidebarBuilder: (context, controller) {
    return CustomSidebar(controller: controller);
  },

  // Custom body/records pane - replace the default table view
  customBodyBuilder: (context, controller, operations) {
    return CustomBody(
      controller: controller,
      operations: operations,
    );
  },

  // Custom record details view
  customDetailsBuilder: (context, controller, operations, resource, record) {
    return CustomDetails(
      controller: controller,
      operations: operations,
      resource: resource,
      record: record,
    );
  },

  // Custom edit dialog
  customEditDialogBuilder: (context, controller, operations, resource,
      currentValues, onSubmit) {
    return CustomEditDialog(
      resource: resource,
      currentValues: currentValues,
      onSubmit: onSubmit,
    );
  },

  // Custom delete confirmation dialog
  customDeleteDialogBuilder: (context, controller, operations, resource,
      record, onConfirm) {
    return CustomDeleteDialog(
      resource: resource,
      record: record,
      onConfirm: onConfirm,
    );
  },

  // Custom create dialog
  customCreateDialogBuilder: (context, controller, operations, resource,
      onSubmit) {
    return CustomCreateDialog(
      resource: resource,
      onSubmit: onSubmit,
    );
  },

  // Custom footer (displayed above the default footer)
  customFooterBuilder: (context, controller) {
    return CustomFooter(controller: controller);
  },

  // Custom themes
  lightTheme: myLightTheme,
  darkTheme: myDarkTheme,
  initialThemeMode: ThemeMode.dark,
)

Available Customization Options

Builder Purpose Parameters
customSidebarBuilder Replace the entire sidebar (context, controller)
customBodyBuilder Replace the records table view (context, controller, operations)
customDetailsBuilder Replace the record details view (context, controller, operations, resource, record)
customEditDialogBuilder Replace the edit dialog (context, controller, operations, resource, currentValues, onSubmit)
customDeleteDialogBuilder Replace the delete confirmation dialog (context, controller, operations, resource, record, onConfirm)
customCreateDialogBuilder Replace the create dialog (context, controller, operations, resource, onSubmit)
customFooterBuilder Add custom footer above default footer (context, controller)

Custom Builder Guidelines

When creating custom builders, you have access to:

  • AdminDashboardController โ€“ Provides access to:

    • resources โ€“ List of all registered resources
    • selectedResource โ€“ Currently selected resource
    • loading โ€“ Loading states
    • themeMode โ€“ Current theme mode
    • Methods to load data, refresh, etc.
  • HomeOperations โ€“ Provides CRUD operations:

    • list() โ€“ Get list of records
    • find() โ€“ Find a specific record
    • create() โ€“ Create a new record
    • update() โ€“ Update an existing record
    • delete() โ€“ Delete a record
  • AdminResource โ€“ Information about the resource:

    • key โ€“ Resource identifier
    • tableName โ€“ Database table name
    • columns โ€“ List of column definitions

Example: Custom Sidebar

Widget CustomSidebar(AdminDashboardController controller) {
  return Drawer(
    child: ListView(
      children: [
        const DrawerHeader(
          decoration: BoxDecoration(color: Colors.blue),
          child: Text('My Admin Panel'),
        ),
        ...controller.resources.map((resource) {
          final customization = controller.sidebarItemCustomizations?[resource.key];
          return ListTile(
            leading: Icon(customization?.icon ?? Icons.table_chart),
            title: Text(customization?.label ?? resource.tableName),
            selected: controller.selectedResource?.key == resource.key,
            onTap: () => controller.selectResource(resource),
          );
        }),
      ],
    ),
  );
}

Example: Custom Edit Dialog

Widget CustomEditDialog({
  required AdminResource resource,
  required Map<String, String> currentValues,
  required Future<bool> Function(Map<String, String> payload) onSubmit,
}) {
  final formKey = GlobalKey<FormState>();
  final controllers = currentValues.map(
    (key, value) => MapEntry(key, TextEditingController(text: value)),
  );

  return AlertDialog(
    title: Text('Edit ${resource.tableName}'),
    content: Form(
      key: formKey,
      child: SingleChildScrollView(
        child: Column(
          children: resource.columns.map((column) {
            return TextFormField(
              controller: controllers[column.name],
              decoration: InputDecoration(labelText: column.name),
              enabled: !column.isId, // Disable editing ID fields
            );
          }).toList(),
        ),
      ),
    ),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context),
        child: const Text('Cancel'),
      ),
      TextButton(
        onPressed: () async {
          if (formKey.currentState!.validate()) {
            final payload = controllers.map(
              (key, controller) => MapEntry(key, controller.text),
            );
            final success = await onSubmit(payload);
            if (success && context.mounted) {
              Navigator.pop(context);
            }
          }
        },
        child: const Text('Save'),
      ),
    ],
  );
}

๐Ÿ”’ Security & Access Control

Role-Based Access Control

Serverpod Admin implements strict role-based access control:

  • โœ… Admin-Only Access โ€“ By default, all access requires the serverpod.admin scope
  • โœ… Secure by Default โ€“ Without admin privileges, users cannot access any part of the admin panel
  • โœ… Authentication Required โ€“ The dashboard automatically shows a login screen for unauthenticated users
  • โœ… Scope Validation โ€“ Users must have the serverpod.admin scope to access any admin functionality

Important: Without the serverpod.admin scope, users will see an error message and cannot access the admin panel, even if they successfully authenticate with email/password.

Login Flow

  1. User enters email and password on the login screen
  2. Authentication is handled via serverpod_auth_idp (EmailAuthController)
  3. Upon successful authentication, the system checks for the serverpod.admin scope
  4. If the user has admin scope, they're granted access to the dashboard
  5. If the user lacks admin scope, they see an error: "User does not have admin privileges"

๐Ÿ”ฎ What's Next?

This is a proof of concept that's already stable and production-ready. We're actively working on:

  • โœ… Export/Import โ€“ Data portability built-in
  • โœ… Role-Based Access โ€“ Secure your admin panel (โœ… Implemented)
  • โœ… Comprehensive Testing โ€“ Ensuring reliability

๐Ÿ’ก The Vision

Serverpod Admin fills the gap that every Serverpod developer has felt. No more custom admin code. No more database dives. Just a beautiful, powerful admin panel that works out of the box.

Welcome to the future of Serverpod administration. ๐ŸŽ‰