Write, scaffold, explain, and debug plugins for the Pelican gaming panel. Use this skill whenever the user mentions Pelican plugins, extending Pelican, FilamentPHP resources or pages for Pelican, plugin service providers, custom permissions, plugin settings, routes, models, widgets, or asks how to add new functionality to the Pelican panel.
Pelican is an open-source game server management panel built on Laravel + FilamentPHP. Plugins let you add functionality without touching core files.
⚠️ The plugin system is still in active development — some features may change.
📖 Detailed Guides:
Run inside the panel directory (/var/www/pelican by default):
php artisan p:plugin:make
This creates the basic structure with plugin.json, main plugin class, service provider, and config file.
Critical: The plugin folder name must exactly match the
idfield inplugin.json.
plugins/my-plugin/
├── plugin.json # Metadata and configuration
├── config/
│ └── my-plugin.php # Config values (use env vars)
├── database/
│ └── migrations/ # Auto-discovered migrations
├── lang/ # Translations (namespaced: my-plugin::strings.key)
├── resources/
│ └── views/ # Blade views (namespaced: my-plugin::view-name)
├── routes/ # Optional route files
└── src/ # App logic (PSR-4 autoloaded)
├── MyPlugin.php # Main plugin class
├── Filament/
│ ├── Admin/ # Admin panel components
│ │ ├── Pages/
│ │ ├── Resources/
│ │ └── Widgets/
│ ├── App/ # Server list panel
│ └── Server/ # Server management panel
├── Models/
├── Policies/ # Auto-discovered
├── Providers/ # Auto-discovered service providers
├── Console/Commands/ # Auto-discovered artisan commands
└── Http/
└── Controllers/
Everything in standard Laravel locations is auto-discovered: migrations, providers, commands, policies.
{
"id": "my-plugin",
"name": "My Plugin",
"author": "Your Name",
"version": "1.0.0",
"description": "Short description",
"category": "plugin",
"namespace": "MyName\\MyPlugin",
"class": "MyPlugin",
"panels": ["admin", "server"],
"panel_version": "^1.2.0",
"composer_packages": {
"vendor/package": "^1.0"
}
}
| Field | Required | Notes |
|---|---|---|
id | ✅ | Must match folder name |
namespace | ✅ | PHP namespace root (use \\ for backslashes) |
class | ✅ | Main class name (in src/) |
category | ✅ | plugin, theme, or language |
panels | No | Array of panel IDs or omit for all panels |
panel_version | No | Minimum panel version (e.g., ^1.2.0) |
composer_packages | No | External dependencies |
Located in src/{ClassName}.php:
namespace MyName\MyPlugin;
use Filament\Contracts\Plugin;
use Filament\Panel;
class MyPlugin implements Plugin
{
public function getId(): string
{
return 'my-plugin';
}
public function register(Panel $panel): void
{
$id = str($panel->getId())->title(); // "Admin", "App", "Server"
// Auto-discover Filament components
$panel->discoverPages(
plugin_path($this->getId(), "src/Filament/$id/Pages"),
"MyName\\MyPlugin\\Filament\\$id\\Pages"
);
$panel->discoverResources(
plugin_path($this->getId(), "src/Filament/$id/Resources"),
"MyName\\MyPlugin\\Filament\\$id\\Resources"
);
$panel->discoverWidgets(
plugin_path($this->getId(), "src/Filament/$id/Widgets"),
"MyName\\MyPlugin\\Filament\\$id\\Widgets"
);
}
public function boot(Panel $panel): void
{
//
}
}
| Panel ID | Area | Use Case |
|---|---|---|
admin | Admin area | Full CRUD for resources, settings, management |
app | Server list | Minimal UI (no nav by default) |
server | Server management | Tenant-scoped (current server context) |
app Paneluse App\Filament\App\Resources\Servers\ServerResource;
use App\Enums\CustomizationKey;
public function register(Panel $panel): void
{
parent::register($panel);
if ($panel->getId() === 'app') {
ServerResource::embedServerList();
$panel->navigation(true);
$panel->topbar(function () {
$nav = user()?->getCustomization(CustomizationKey::TopNavigation);
return in_array($nav, ['topbar', 'mixed', true], true);
});
$panel->clearCachedComponents();
}
}
Call static methods on core classes inside a service provider's register():
use App\Filament\Admin\Resources\Users\UserResource;
use App\Filament\Admin\Resources\Servers\ServerResource;
use App\Models\Role;
public function register(): void
{
// Add a relation manager tab
ServerResource::registerCustomRelations(MyRelationManager::class);
// Register permissions
Role::registerCustomDefaultPermissions('myModel');
Role::registerCustomModelIcon('myModel', 'tabler-star');
}
Available customization traits (check app/Traits/Filament/ for all):
CanModifyResource - Relation managers, custom actionsCanCustomizePage - Widgets, header actionsCanModifyForm / CanModifyTable - Form/table hooksImplement HasPluginSettings on your main class:
use App\Contracts\Plugins\HasPluginSettings;
use App\Traits\EnvironmentWriterTrait;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
class MyPlugin implements Plugin, HasPluginSettings
{
use EnvironmentWriterTrait;
public function getSettingsForm(): array
{
return [
TextInput::make('api_key')
->required()
->default(fn () => config('my-plugin.api_key')),
];
}
public function saveSettings(array $data): void
{
$this->writeToEnvironment([
'MY_PLUGIN_API_KEY' => $data['api_key'],
]);
Notification::make()->title('Settings saved')->success()->send();
}
}
Always prefix env vars with your plugin ID to avoid conflicts.
In your service provider's register():
use App\Models\Role;
// Shorthand: registers viewList, view, create, update, delete
Role::registerCustomDefaultPermissions('myModel');
// Custom permissions
Role::registerCustomPermissions([
'myModel' => ['export', 'approve'],
'server' => ['customAction'], // extend existing model
]);
// Optional: icon for permission group
Role::registerCustomModelIcon('myModel', 'tabler-star');
use App\Models\Subuser;
// New permission group
Subuser::registerCustomPermissions('myFeature', ['read', 'write'], 'tabler-bolt', false);
// Append to existing group
Subuser::registerCustomPermissions('console', ['myCustomAction']);
Create a RouteServiceProvider in src/Providers/:
use Illuminate\Foundation\Support\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Route;
class MyPluginRoutesProvider extends RouteServiceProvider
{
public function boot(): void
{
$this->routes(function () {
// Simple route
Route::get('/my-plugin/test', [TestController::class, 'index'])
->name('my-plugin.test');
// Load from file
Route::prefix('/my-plugin')
->group(plugin_path('my-plugin', 'routes/web.php'));
// Append to client API
Route::middleware(['api', 'client-api', 'throttle:api.client'])
->prefix('/api/client/servers/{server}')
->scopeBindings()
->group(plugin_path('my-plugin', 'routes/api-client.php'));
});
}
}
In your service provider's boot():
use App\Models\Server;
use MyPlugin\Models\Ticket;
public function boot(): void
{
Server::resolveRelationUsing('tickets', fn (Server $server) =>
$server->hasMany(Ticket::class, 'server_id', 'id')
);
}
Now $server->tickets works everywhere.
use App\Policies\DefaultAdminPolicies;
class MyModelPolicy
{
use DefaultAdminPolicies;
protected string $modelName = 'myModel';
}
This automatically checks admin role permissions based on the registered model name.
Place in lang/{locale}/ (e.g., lang/en/strings.php):
return [
'welcome' => 'Welcome',
'item' => 'Item|Items', // Pluralization
];
Usage:
trans('my-plugin::strings.welcome')
trans_choice('my-plugin::strings.item', 2) // "Items"
Place in resources/views/:
view('my-plugin::my-view')
// → plugins/my-plugin/resources/views/my-view.blade.php
See FilamentPHP Patterns for:
See Advanced Patterns for:
See Complete Plugin Walkthrough for a step-by-step guide building a "Server Notes" plugin.
plugin.json and remove the meta block (internal use only)plugins/Publish to the community:
#plugins channel at discord.gg/pelican-panel\\ (double backslash) for namespace separators001_, 002_) to control execution orderMY_PLUGIN_*)Filament::getTenant() to get current server in server panelregisterCustomRelations() in service provider's register() methodphp artisan migrate:fresh --seed to reset and test migrationsregister(), not boot()