smallruraldog/laravel-vue-admin
Laravel Vue Admin is a full-featured admin panel starter built with Laravel and Vue.js. It bundles authentication, role and permission management, menu and CRUD scaffolding, and a clean UI, so you can build back-office systems quickly without wiring boilerplate.
Requirements: PHP 7.x, Laravel 6–8 era, MySQL. The package ships a compiled Vue 2 + Vuex + Vue Router + Element UI SPA, so you do not need Node to get a working admin.
composer require smallruraldog/laravel-vue-admin
php artisan vendor:publish --provider="LaravelVueAdmin\LaravelVueAdminServiceProvider"
php artisan admin:install # migrations + seed (admin/admin) + publish assets
php artisan storage:link # only if you use image/file fields
Then log in at /admin (prefix is configurable). First smoke test: open the built-in Menu, Roles, Permissions, and Users screens — if those render, the API layer, auth guard, and SPA are wired correctly.
Where to look first, in this order:
config/admin.php — route prefix/namespace/middleware, auth.guard / auth.provider, upload disk, extension hooks. This file is the whole control panel of the control panel.app/Admin/Controllers/* and app/Admin/routes.php — the skeleton the installer gives you is the canonical pattern.vendor/smallruraldog/laravel-vue-admin/src/Form, src/Grid, src/Show — because the docs are thin, the field classes are the API reference. ls src/Form/Field tells you exactly which widgets exist and their method signatures.First use case — a CRUD screen in one file. Generate or hand-write a controller that returns a Grid for index, a Form for create/edit, a Show for detail, register one resource route and one menu entry via the Menu UI. That covers ~80% of admin work; everything else is variations on it.
Controller shape. Your admin controllers extend the package's base controller and return Content objects; the SPA fetches the field schema over JSON and renders it.
class PostController extends AdminController
{
public function index(Content $content)
{
return $content->title('Posts')->body($this->grid());
}
protected function grid()
{
$grid = new Grid(new Post());
$grid->id('ID')->sortable();
$grid->title('Title')->filter('like');
$grid->author()->display(fn ($v) => $v->name ?? '-'); // executed server-side per row
$grid->status('Status')->using([0 => 'Draft', 1 => 'Live'])->badge();
$grid->created_at('Created')->sortable();
$grid->filter(function ($filter) {
$filter->like('title');
$filter->between('created_at')->datetime();
});
$grid->actions(fn ($a) => $a->disableView());
return $grid;
}
protected function form()
{
$form = new Form(new Post());
$form->text('title', 'Title')->rules('required|max:120');
$form->select('author_id', 'Author')->options(User::pluck('name', 'id'));
$form->editor('body', 'Body');
$form->image('cover', 'Cover')->uniqueName()->removable();
$form->switch('status', 'Published');
$form->saving(function (Form $form) {
$form->model()->slug = Str::slug($form->title);
});
return $form;
}
}
Grid patterns that pay off immediately
$grid->model()->where('tenant_id', Admin::user()->tenant_id) — do this in a base controller so every screen is scoped by construction, not by memory.$grid->model()->with('author'); display() closures fire per row and will N+1 you into a slow list view otherwise.->sortable(), ->quickSearch(), ->perPage(50), ->filter() are cheap wins users ask for on day one.disableCreation(), disableEdit(), disableDelete()) rather than deleting code paths.Form patterns
->rules()), business logic in saving/saved hooks — the same split as Form Requests + Observers, but co-located.hasMany / embeds style field groups for child records instead of building a second screen.->image() / ->multipleImage() / ->file() all go through the upload disk in config/admin.php; set uniqueName() or you will overwrite files on re-upload.$form->html(...) or a view body — $content->body(view('admin.report', $data)) is the escape hatch for dashboards and reports that aren't CRUD.Navigation, ACL, and custom fields
admin_menu / admin_permissions / admin_roles tables) and attach permission middleware to controllers. Add menu entries for new controllers, otherwise the screen exists but nobody can find it.$form->myWidget('field') behaves like any other field. This is the right investment once you need the same bespoke widget (map picker, media library, JSON editor, status timeline) across five screens — but it requires the front-end source and a Node build, which is exactly the cost the package claims to remove. Budget for it deliberately.grep src/Form/Field and src/Grid/Column — that's faster than searching the README./admin/api/... XHR → read the JSON message/trace, then check storage/logs/laravel.log. Never debug from the UI alone.Administrator model, its own table, and its own guard from config/admin.php. Creating an admin with App\Models\User does nothing. Pointing auth.provider at your app's user provider works only if that model matches the expected contract.APP_URL is wrong, storage:link is missing, the disk isn't public, or PHP's upload_max_filesize/post_max_size is under the file size — the field just renders empty. Check all four before touching code.php artisan route:cache breaks if any admin route uses closures. Keep admin routes controller-based if you cache routes in production.php artisan cache:clear before changes appear.config/app.php timezone while the front-end formats with the browser's locale. Mismatches on created_at/datetime fields are usually config, not the package.->editor() and ->html() fields are stored and re-rendered as HTML. Sanitize on save or you have stored XSS with admin-level blast radius.vendor:publish overwrites. Keep config/admin.php and any published assets in git, and re-check diffs after every composer update — which, given the release cadence, should be rare and deliberate.$content->body() is cheaper than a custom Vue component and survives front-end rebuilds.How can I help you explore Laravel packages today?