classic-o/nova-media-library
A Laravel Nova media library package for browsing, uploading, organizing, and selecting media files in a clean Nova interface. Manage assets and attach them to resources without leaving the admin panel.
Architecture fit. This is a UI adapter, not a platform component. The package contributes exactly two things to a Laravel Nova app: a sidebar tool (browse/upload modal) and a form field (attach media to a resource), both sitting on top of spatie/laravel-medialibrary, which owns the media table, the disk abstraction, and the conversion pipeline. That is a thin, well-bounded seam — the package has no opinion about storage, no queue of its own, no background services, and no external API surface. For a TPM this is the good kind of dependency: it fails locally (one field renders blank) rather than systemically (checkout breaks). It also means the value proposition is entirely conditional on two things already being true: you run Nova (a paid license), and you are willing to adopt Medialibrary as your asset persistence layer. If either is false, the evaluation ends here — there is nothing to pilot.
Integration feasibility. High for a small team already on the stack, moderate otherwise. The integration work is genuinely a day, not a sprint: publish Spatie's migrations and config, register the tool in NovaServiceProvider::tools(), add one nullable foreign-key column, attach the field to one resource, and run the upload→attach→render→delete loop by hand. The assessment material is explicit that the package source is small enough to read in full, which materially de-risks the "what does this actually do" question a TPM normally has to fund a spike to answer. The realistic cost estimate is 1–3 days to a working pilot on one resource, plus 1–2 days of validation and runbook writing before it goes near editors. That is the number to put in a plan, not "add a Composer dependency."
Technical risk. Concentrated in one place and it is not the package's feature set — it is Nova major-version compatibility. The package is built against older Nova field/tool conventions; Nova 4/5 rewrote the front-end layer. The failure mode is a blank modal, a 404 on compiled JS, or a silently no-op'd chainable modifier — none of which throw, all of which look like configuration errors. Secondary risks, in descending order of how much they cost if missed:
queue_conversions_by_default + a worker), not a code change, but it is a "the tool is broken" bug report if unaddressed.Key questions before committing.
File field is cheaper and carries no orphan risk.assets/files table already exist? If so, which wins — and is media an adapter or a second source of truth?Stack fit. Requires Laravel + Nova + PHP, with spatie/laravel-medialibrary as a hard transitive dependency. It fits cleanly on an existing Nova admin: no new infrastructure, no new service, no new credentials beyond whatever disk you already use (local, S3, or equivalent). The one environmental prerequisite that catches teams is the image driver — missing Imagick fails at upload with an opaque error. Confirm GD or Imagick in the container before the pilot, not during it.
Migration path. Two distinct migrations, and they should not be conflated:
Greenfield (no media today): publish Spatie migrations → make three deliberate config decisions (disk_name, path_generator, queue_conversions_by_default) → register the tool → add a nullable indexed FK column → attach the field to exactly one resource → hand-run the full loop → write the integration test → widen to a second resource. Roughly a week of elapsed calendar time including review, a day of actual work.
Brownfield (existing path/URL columns): additive only. Create media rows for legacy files via a one-off command, write the new ID back into a new column, keep both columns live during a dual-run window, cut reads over to a single accessor (getCoverUrlAttribute()), then drop the old column. Zero downtime, reversible at every step. The accessor is the highest-leverage decision in the whole integration: change disks, add a CDN, fork the field — you edit one method and every consumer (Blade, API resources, emails, sitemaps) follows.
Compatibility. Pin the package to an exact version in composer.json — not a caret range. A floating constraint plus a Nova minor bump is precisely how the admin panel breaks on a Tuesday with no deploy to blame. Validate the whole matrix (Nova major × Laravel × PHP × package tag) in a scratch app rather than trusting the README; the README describes an API that has drifted. The two things to verify in vendor/classic-o/nova-media-library/src/ before writing any application code are the class names/constructor argument order for the field, and which chainable modifiers (multiple(), disk(), folder()) actually exist in the installed tag.
Sequencing. Deliberately narrow, and resist compressing it:
media row after the first save to confirm the persisted shape.Storage::fake(). This is what makes the next Nova bump fail loudly instead of silently degrading.medialibrary:regenerate, how to find orphans, and how to restore DB and bucket as a unit.Maintenance. Low run-rate, spiky at upgrade boundaries. No daemon, no cron of its own, no versioned API to track. The recurring obligations are: keep the exact version pin honest, re-run php artisan nova:publish after any Nova change, and treat each Nova major as a planned migration with the integration test as the gate. Because the source is small and forkable, the downside is bounded — but only if someone is actually watching for the next Nova major. Name that person.
Support. Support load falls almost entirely on editors, and it is predictable: "the upload spinner hangs" (unqueued conversions), "the image 404s on the site but previews fine in Nova" (missing storage:link or S3 CORS/bucket policy), "the gallery saved empty" (persistence-format mismatch), "the modal is blank" (Nova asset mismatch). Each has a known first-look. Write those four into the runbook and the ticket volume collapses. Engineering support is minimal after the pilot except at upgrade time.
Scaling. The package itself does not scale — it is a UI layer. What scales is Medialibrary, and the levers are all config and model-level: queue_conversions_by_default plus workers for upload throughput, a Media subclass so conversions compute once when several resources consume the same rows, a deliberate path_generator, and a CDN in front of whichever disk you chose. The genuine scaling failure is not request volume, it is storage monotonicity — orphaned rows and orphaned files accumulate silently in both directions and the bucket only grows. A scheduled sweep (rows with no owner; files with no row) is the difference between a managed library and a bill you notice in month eighteen. Decide the retention policy — "sweep unreferenced media after N days" — before content volume makes the decision for you.
Failure modes, ranked by likelihood × cost.
| Failure | Likelihood | Cost | Mitigation |
|---|---|---|---|
| Nova major mismatch → blank modal | High on upgrade | Editor-blocking, no exception thrown | Exact version pin + integration test in CI |
| Silent persistence-format drift → empty gallery | Medium | Silent data loss of attachments | Feature test asserting persisted shape after first save |
| Inline conversion → modal timeout on large uploads | High if unconfigured | Editors abandon the tool | queue_conversions_by_default + worker; nonQueued() only for small conversions |
| Orphan accumulation | Certain over time | Storage cost, dangling references | Scheduled bidirectional sweep + written policy |
| Shared media deleted on parent delete | Medium, high blast radius | Broken images across many records | Never cascade-delete library media; delete only when the record owns it |
| Unvalidated upload (SVG/zip) | Medium | Stored XSS if served same-origin | accepts_mime_types, size caps, admin-only tool until audited |
| Partial restore (DB restored, bucket not) | Low frequency, high cost | Dangling media rows pointing at nothing | Document DB + bucket as one restore unit |
Ramp-up. Two audiences, two curves. For editors, the ramp is a workflow change, not a tool tutorial: the entire point of the tool is reuse, so the habit to instil is "search the library before uploading." That is a one-page guideline and a nudge in the tool's own UI, but it must land before per-record uploads accumulate — retrofitting a shared library onto thousands of record-specific files is a data migration, not a settings change. For engineering, ramp is roughly half a day: read the package source (it is small), read config/media-library.php, understand the accessor pattern and the deletion policy. That is unusually cheap for an admin-panel dependency.
Bottom line for planning. This is a schedule-risk reduction play for the next content-heavy release — it turns "rich content with images and attachments" from a one-to-three-sprint custom build into roughly a week of integration — purchased with a small, forkable bet on a low-activity package tracking a fast-moving commercial framework. Fund the spike, pin the version, write the test, write the runbook, and assign an owner for Nova upgrades. Do those five things and the bet is well-priced. Skip them and you will discover the Nova mismatch in production.
How can I help you explore Laravel packages today?