livewire-filemanager/filemanager
Livewire-powered file manager for Laravel with drag-and-drop uploads, search, dark mode, multi-language support, and API endpoints. Integrates with spatie/laravel-medialibrary for easy file and folder management.
The package is a drop-in Livewire component, so the honest minimum is: install, migrate, mount one component, and put it behind middleware.
composer require livewire-filemanager/filemanager
php artisan vendor:publish --tag=livewire-filemanager-migrations
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate
Then mount it on a real route — not in a layout you already ship — so you can decide who reaches it:
// routes/web.php
Route::middleware(['auth', 'can:manage-assets'])
->get('/admin/files', fn () => view('admin.files'))
->name('admin.files');
{{-- resources/views/admin/files.blade.php --}}
<x-app-layout>
<x-livewire-filemanager />
</x-app-layout>
Make sure Tailwind actually compiles the package's views, otherwise you get a functional but unstyled grid:
// Tailwind v4, resources/css/app.css
@source '../../vendor/livewire-filemanager/filemanager/resources/views/**/*.blade.php';
// Tailwind v3, tailwind.config.js
content: [
'./resources/**/*.blade.php',
'./vendor/livewire-filemanager/filemanager/resources/views/**/*.blade.php',
],
If you want files reachable by direct URL, register the package's controller route — and register it last, since {path} is a catch-all:
use LivewireFilemanager\Filemanager\Http\Controllers\Files\FileController;
Route::get('{path}', [FileController::class, 'show'])
->where('path', '.*')
->name('assets.show');
Where to look first: don't start with docs.md. Start with config/livewire-filemanager.php after publishing it (--tag=livewire-filemanager-config) and with config/media-library.php. Those two files answer 80% of the "why is this behaving like that" questions. Then skim the folders migration so you know the table shape you'll be querying from your own code.
First use case to ship: an internal admin media library. Upload assets for a CMS or marketing site, browse by folder, copy a URL, paste it into a content field. It's the lowest-authorization-risk use case (staff only, no tenant boundary), it proves the storage/queue/URL story end to end, and it's typically a sprint you don't want to spend.
Treat the component as a page, not a widget. It owns its own layout, routing state, search, and drag-and-drop. Embedding it in a tabbed settings screen or a modal tends to fight the component's own Livewire lifecycle and Tailwind assumptions. Give it a dedicated admin route with a layout and let it be the page.
Authorization is yours, so make it explicit at two layers. The route middleware is the first layer. The second is ACL inside the package:
php artisan vendor:publish --tag=livewire-filemanager-config
# set 'acl_enabled' => true
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"
# set 'media_model' => LivewireFilemanager\Filemanager\Models\Media::class
acl_enabled is what makes "only the user who created a file can see it" true. Without it the manager is a shared bucket. If you already have your own Media model, don't swap media_model globally — apply LivewireFilemanager\Filemanager\Traits\HasMediaOwner to your existing model instead. Swapping the global media model on a project that already uses medialibrary is a migration, not a config tweak.
Multi-tenant: scope before you ship. There's no tenant column in the package's model. The practical patterns are (a) one filemanager route per tenant with a tenant scope resolved in middleware plus a global scope on Folder, or (b) ACL on top of per-user ownership if your tenancy is effectively single-user-per-tenant. Decide which before the first upload, because retrofitting a tenant_id into folders and media after launch means backfilling both tables.
Consuming uploaded assets from your own app code. Once files exist, treat the package's media records as the source of truth and query them rather than re-implementing storage:
use LivewireFilemanager\Filemanager\Models\Media;
use LivewireFilemanager\Filemanager\Models\Folder;
$folder = Folder::where('name', 'hero-images')->first();
$urls = Media::where('folder_id', $folder->id)
->get()
->map(fn (Media $m) => $m->getUrl());
For a picker UI, build your own small Livewire component that lists Media and writes the chosen ID or URL into the parent form. Don't try to make <x-livewire-filemanager /> return a selection — it isn't a picker, it's a manager.
Public delivery: pick one strategy and be consistent. Either expose assets.show and let the package stream files, or skip the catch-all route entirely and generate signed, time-limited URLs in your own controller. The second is usually the right call for anything user-uploaded, and it avoids the {path} catch-all swallowing unrelated routes.
API for SPA/mobile/automation. Endpoints live under /api/filemanager/v1/ and authenticate via Sanctum. This is the pattern that pays off when a mobile app or an import script needs to push files without a browser:
$token = $user->createToken('importer', ['filemanager:upload'])->plainTextToken;
Http::withToken($token)
->attach('file', $path, $name)
->post(config('app.url').'/api/filemanager/v1/files', ['folder_id' => $folderId]);
If you're not using it, disable it in config — an authenticated-but-unused API surface is pure attack surface. Note the API config keys are added over time, so re-publish/merge the config file when you upgrade.
Localization and theming as extension points. The package ships translations under resources/lang; publish them (--tag=livewire-filemanager-translations) to correct wording for your domain ("Assets" instead of "Files") without patching vendor code. Dark mode is already handled — don't add your own toggle on top of it.
@filemanagerStyles pulls the Tailwind Play CDN. The README says it plainly: not for production. Use it during the first five minutes of local exploration, then remove it and wire the vendor views into your Tailwind build. If the component looks unstyled in staging but fine locally, this is almost always why — your production build purged classes it never saw in resources/views.
Thumbnails are queued, so "thumbnails don't work" usually means "no worker". Either run php artisan queue:work or set QUEUE_CONNECTION=sync in local. Failed conversions land in failed_jobs; php artisan medialibrary:regenerate rebuilds them after you fix the disk or the image driver. Check that GD or Imagick is actually installed — a missing extension produces silent conversion failures rather than a loud error.
The media_model swap is global and retroactive. If your app already stores media via spatie/laravel-medialibrary, changing media_model to the package's model detaches your existing rows from the model you're querying. The HasMediaOwner trait is the safer route. Test this on a database dump before touching production.
Two migration publishes, and ordering matters. If medialibrary is already installed you'll get a "table already exists" on media. Check migrations/ before running the publish, and if you're retrofitting, reconcile the existing media table against the package's expectations rather than dropping it.
The {path} route shadows everything below it. Registered early, it will happily match /login, /dashboard, and your API routes. Put it at the bottom of web.php, or skip it and serve through signed URLs. Also verify APP_URL matches the real host — the "copy URL to clipboard" button builds absolute URLs, and the README requires HTTPS for it to behave.
Upgrading from 0.x is a rename, not just a bump. v1.0.0 renamed every livewire-fileuploader reference to livewire-filemanager: publish tags, config filename and keys (config('livewire-fileuploader.*') → config('livewire-filemanager.*')), published view and translation namespaces. Run php artisan filemanager:migrate-config, then grep your whole codebase for livewire-fileuploader — a stale config() call is a silent null, not an exception. Pin the version in composer.json while you're at it; the package explicitly warns its structure can change.
The package ships no security layer for uploaded files. This is the single biggest gotcha and it's stated in the README. Concretely, that means you own: authentication on the route, per-user isolation (acl_enabled), upload validation and MIME whitelisting, malware scanning, quotas and disk-space monitoring, retention/deletion policy, audit logging of who uploaded and deleted what, and the decision about whether files are publicly reachable at all. Budget this as a companion workstream, not a follow-up ticket.
Debugging checklist when the component misbehaves. storage/logs/laravel.log first (Livewire exceptions surface there with the full trace). Then the browser network tab for the Livewire update request — a 419 means session/CSRF, a 403 means your middleware, a 500 means look at the trace. Then confirm the storage disk in config/media-library.php — local with private visibility plus the package's FileController works; s3 needs the streaming route, not getUrl(). And if the UI renders but drag-and-drop doesn't, Alpine isn't loaded — @filemanagerScripts must be present exactly once, and Alpine must not be double-initialized by your own bundle.
It's one folders table and one media table — verify before you commit. There are zero dependents and the API/config surface is still growing. Before adopting, run the install against a throwaway branch, upload a folder tree 3 levels deep, delete a folder with contents, and confirm the media records and files on disk are actually removed. Deleting parent folders is exactly the kind of thing that gets fixed in a patch release, and you want to know where you stand before your users find out for you.
How can I help you explore Laravel packages today?