moonshine/app
Core application package for MoonShine, the Laravel admin panel, providing the foundation for building admin interfaces with resources, CRUD screens, forms, filters, and extensible components that integrate smoothly with Laravel.
Install (fresh Laravel 11/12 app)
composer require moonshine/moonshine
php artisan moonshine:install
php artisan moonshine:user # create the first admin
Then log in at /admin with the credentials you just created. Everything below assumes that flow worked — if /admin 404s, the service provider isn't registered (see Gotchas).
Where to look first
config/moonshine.php — auth guard, route prefix, middlewares, layout/theme, cache settings.MoonShineServiceProvider (registered in bootstrap/providers.php) — this is where resources are registered via $core->resources([...]) and where the menu is built via $core->menu(...) / MenuManager.app/Moonshine/Resources/ — the generated MoonShineUserResource is your reference implementation; copy its structure rather than reading docs cold.vendor/moonshine/moonshine/src/UI/Fields/ — the canonical list of available field types.First use case: one resource, end to end
php artisan moonshine:resource Post
Edit app/Moonshine/Resources/PostResource.php: set protected string $model = Post::class;, fill fields() with ID::make()->sortable(), Text::make('Title'), Textarea::make('Body'), BelongsTo::make('Author', 'author', resource: UserResource::class) — and make sure it's registered in the service provider. Menu entry, index listing, create/edit form, detail page, and delete all exist at that point. That single resource is the fastest way to learn the framework's shape; everything else is a variation on it.
Resource as the unit of work. A ModelResource is the join point for fields(), pages() (Index/Form/Detail), filters(), search(), $with (eager loading), and $sortable. Prefer overriding the resource over writing custom controllers — you rarely need a controller at all.
Field sets and reuse. Extract field arrays to traits or static builders when the same model appears in multiple resources or in a nested relationship. Keep field names identical to the model attribute or accessor name; Moonshine maps them positionally on save, so a mismatch fails silently as "field not saved" rather than throwing.
Conditional UI instead of conditional logic. ->canSee(fn() => user()->isSuperUser()) on fields, menu items, and action buttons is the idiomatic way to gate features. Pair it with Laravel policies on the underlying model for defense in depth — canSee is UI-only and does not protect the endpoint.
Actions. ActionButton::make('Publish')->method('publish') on Detail/Index pages, with the method living on the resource or page. Bulk actions on IndexPage for anything that loops — one request beats N. For anything longer than a second or two, dispatch a job from the action method and return a toast, rather than blocking the admin request.
Filters. Declare on IndexPage::filters(), and drop to QueryTag (or an explicit closure) the moment the filter needs a whereHas or a date range on a joined column. Custom filters are a supported extension point, not a hack.
Custom pages and components. Extend Page/CrudPage, define components(), and register the page alongside resources. Custom components extend Component and render a Blade view — this is the clean way to embed charts, dashboards, or third-party widgets without fighting the default layout.
Menu as configuration, not code paths. Build with Menu::make([...]), MenuGroup, and canSee closures. Because the menu is registered in the service provider, it caches well but does not hot-reload in production — see below.
Auth. Moonshine ships its own moonshine guard and moonshine_users table, separate from your app's users. In tests, use $this->actingAs($admin, 'moonshine'); in application code, auth('moonshine')->user() — auth()->user() will return the wrong user or null and cause confusing null-pointer failures inside field closures.
Deployment. Treat the admin panel like any other frontend asset: publish/build whatever theme customization you added, run php artisan optimize (config + route + view cache), and confirm the asset step ran in CI. Missing assets present as an unstyled but otherwise functional panel, which is easy to miss in a smoke test.
moonshine guard. Null users inside canSee usually mean this, not a broken login.users table expecting single sign-on; either seed admins into moonshine_users or write a deliberate custom guard/provider. Half-migrating this later is painful.datetime column, array into a non-JSON column), often saves nothing and reports nothing. When a field "doesn't persist," check the name and the cast first.indexQuery/$with is your lever. BelongsTo columns on the index are the usual culprit; a listing that takes 3s locally will time out in production."moonshine/moonshine": "~4.0" (or a specific 4.0.x) until you've read the compare diff, run your test suite against a staging copy, and clicked through your most customized resource and one custom page. Watch specifically for internal Blade view or CSS class changes affecting any theme overrides you carry.Field subclasses (for a repeated input pattern), custom Component + Blade view, custom Page, custom filter classes, and the service provider itself for cross-cutting registration. Almost every "I need to hack the layout" instinct is better served by a custom component.->canSee(false) and dd() inside a field closure remain the fastest way to isolate whether the problem is registration, authorization, or rendering.actingAs on the moonshine guard, hit the resource index, assert assertOk and a known column) catch the majority of upgrade breakage cheaply. Add one before you upgrade to 4.1.0 if you don't have one.How can I help you explore Laravel packages today?