Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Spatie Media Library Complete Guide

By Kokil Thapa | Last reviewed: September 2026

File uploads break down the moment one model needs a hero image, a PDF gallery, and a private client contract. The spatie media library package solves that in Laravel by storing file metadata in a dedicated table and binaries on any configured disk. This guide covers install on Laravel 13, collections, conversions, cloud storage, and the production traps I hit on legal-tech portals and eCommerce builds. If you are evaluating Spatie packages, start with the essential Laravel Spatie plugins overview — Media Library is usually the first one I wire in.

What Is Spatie Media Library and Why Use It in Laravel?

Spatie Laravel Media Library associates one or many files with any Eloquent model. Instead of scattering avatar_path, thumbnail, and document_url columns, you keep metadata in a media table and store binaries on local, public, or S3 disks.

On real client projects — client portals with document sharing, florist shops with hundreds of product images, booking systems with itinerary PDFs — I reach for this package because it handles recurring problems in one place:

  • Multiple files per record — galleries, attachments, versioned uploads.
  • Named collections — separate avatar from documents with different rules.
  • Image conversions — thumbnails, WebP variants, watermarks via queued jobs.
  • Custom properties — JSON metadata (alt text, uploaded-by, expiry) without schema migrations.
  • Disk abstraction — swap local storage for S3 without rewriting controllers.
Spatie Media LibraryEloquent ModelHasMedia traitmedia tablemetadata + pathStorage Disklocal / S3CollectionsConversionsCustom propsOne package replaces ad-hoc file columns and manual resize scripts
Spatie media library — model, database metadata, and filesystem storage

Compared with rolling your own upload handler, Spatie Media Library keeps file logic close to the domain model. That matches how I structure modern Laravel architecture: fat models or dedicated actions, thin controllers, and filesystem config in config/filesystems.php.

Spatie also ships a paid Media Library Pro add-on with drop-zone UI components. The open-source package documented at the official Spatie site covers everything most backends need. Pro helps when you want pre-built Livewire or Vue upload widgets without writing front-end glue.

ApproachProsCons
Path columns on model (photo_path)Fast to prototypeBreaks with multiple files, no conversions, disk logic in controllers
Polymorphic attachments table (DIY)FlexibleYou rebuild conversions, ordering, responsive images
Spatie media libraryCollections, conversions, queues, responsive images built inLearning curve; queue worker required for async conversions
Cloud-only (S3 direct SDK)Scales storageNo Eloquent integration; metadata sync is manual

How Do You Install and Configure Spatie Media Library in Laravel 13?

Laravel 13 requires PHP 8.3 or higher. PHP 8.5 is the current anchor in my stack. Composer 2.10 handles the install. The package supports Laravel 11 through 13. Laravel 12 remains supported until February 2027.

Install the package and publish assets

  1. Require the package with Composer.
  2. Publish and run the migration.
  3. Optionally publish config for disk and queue tuning.
composer require spatie/laravel-medialibrary
php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations"
php artisan migrate

Optional config publish:

php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config"

Filesystem and queue prerequisites

Image conversions need GD or Imagick. On Ubuntu servers I maintain, Imagick gives better quality and memory behaviour:

sudo apt install php8.5-imagick
sudo systemctl reload php8.5-fpm

Configure your default disk in .env. Local public storage works for development. Production apps usually move to S3-compatible storage:

FILESYSTEM_DISK=public
MEDIA_DISK=public

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=ap-south-1
AWS_BUCKET=
AWS_URL=

Run the standard Laravel storage link once per server:

php artisan storage:link

Queued conversions need a worker. Set QUEUE_CONNECTION=database or Redis 8.10 and run Supervisor. I use the same Deployer pattern from my GitLab CI pipeline for Laravel notes: migrate, symlink, reload PHP-FPM, restart queue workers after each release.

How Do You Add File Uploads and Media Collections to Eloquent Models?

The core of any laravel spatie media library setup is wiring models correctly. Any model that accepts uploads implements Spatie\MediaLibrary\HasMedia and uses InteractsWithMedia.

Basic model setup

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;

class Post extends Model implements HasMedia
{
    use InteractsWithMedia;

    protected $fillable = ['title', 'body'];
}

Register collections in registerMediaCollections()

Collections are named buckets with rules: single vs multiple files, accepted MIME types, disk name.

public function registerMediaCollections(): void
{
    $this->addMediaCollection('featured')
        ->singleFile()
        ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);

    $this->addMediaCollection('gallery')
        ->acceptsMimeTypes(['image/jpeg', 'image/png', 'image/webp']);

    $this->addMediaCollection('documents')
        ->acceptsMimeTypes(['application/pdf'])
        ->useDisk('s3');
}

Controller upload patterns

From a validated form request:

public function store(StorePostRequest $request)
{
    $post = Post::create($request->validated());

    if ($request->hasFile('featured')) {
        $post->addMediaFromRequest('featured')
            ->usingFileName($request->file('featured')->hashName())
            ->withCustomProperties(['alt' => $request->input('alt_text')])
            ->toMediaCollection('featured');
    }

    foreach ($request->file('gallery', []) as $image) {
        $post->addMedia($image)->toMediaCollection('gallery');
    }

    return redirect()->route('posts.show', $post);
}

Validation belongs in a Form Request. Never trust client MIME checks alone:

public function rules(): array
{
    return [
        'featured' => ['nullable', 'image', 'max:5120'],
        'gallery.*' => ['image', 'max:5120'],
        'documents.*' => ['file', 'mimes:pdf', 'max:10240'],
    ];
}

For JSON endpoints, map media in an API resource. That mirrors Laravel API best practices. Keep authorisation at the policy layer — see Laravel policies and gates for gating who may upload or delete files on shared records.

Media Upload FlowHTTP Requestmultipart formForm Requestvalidate MIMEControlleraddMedia()Collectionfeatured / docsMedia row inserted — file on diskcustom properties stored as JSONQueue: conversionsBlade / API URL
Spatie laravel media library upload path — validation, collection assignment, async conversions

Retrieving and displaying media

In Blade:

@if($post->getFirstMediaUrl('featured'))
    <img src="{{ $post->getFirstMediaUrl('featured', 'thumb') }}"
         alt="{{ $post->getFirstMedia('featured')?->getCustomProperty('alt') }}"
         class="img-fluid">
@endif

@foreach($post->getMedia('gallery') as $media)
    <img src="{{ $media->getUrl('webp') }}" alt="" loading="lazy">
@endforeach

Common retrieval helpers:

  • getFirstMedia('collection') — returns a Media model instance or null.
  • getFirstMediaUrl('collection', 'conversion') — URL string for templates.
  • getMedia('collection') — ordered collection of all items.
  • hasMedia('collection') — boolean guard before rendering.

On a legal-tech portal I built, separating documents (private disk, policy-gated download route) from public-assets (public disk) prevented accidental exposure of client PDFs. I have seen teams store everything under storage/app/public without thinking through authorisation.

How Do You Create Image Conversions and Responsive Variants?

Conversions are derived files from an original upload: thumbnails, cropped squares, WebP copies, greyscale previews. Register them in registerMediaConversions().

Define conversions on the model

use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Image\Enums\Fit;

public function registerMediaConversions(?Media $media = null): void
{
    $this->addMediaConversion('thumb')
        ->width(300)
        ->height(300)
        ->fit(Fit::Crop, 300, 300)
        ->nonQueued();

    $this->addMediaConversion('webp')
        ->format('webp')
        ->quality(82)
        ->performOnCollections('featured', 'gallery');

    $this->addMediaConversion('preview')
        ->width(1200)
        ->performOnCollections('gallery')
        ->queued();
}

By default, conversions run queued. Use ->nonQueued() only for small thumbnails needed on upload. Otherwise your HTTP request waits on Imagick.

For deeper image tuning, pair this package with the walkthrough on image optimization in Laravel with Spatie Media Library. Front-end lazy loading lives in your Vite bundle — see Vite config for Laravel projects for asset pipeline setup.

Responsive images

Spatie can generate responsive image markup with multiple srcset widths:

public function registerMediaConversions(?Media $media = null): void
{
    $this->addMediaConversion('responsive')
        ->width(1600)
        ->withResponsiveImages();
}

In Blade, output the responsive bundle:

{{ $post->getFirstMedia('featured')?->img('', ['class' => 'img-fluid']) }}

That helps Core Web Vitals on content-heavy sites. I treat it as architecture, not a post-launch patch, when doing technical SEO work alongside development. Product images on florist eCommerce builds benefit from WebP conversions plus responsive srcsets.

Conversion PipelineOriginal UploadJPEG 4000pxthumb 300pxcropped squarewebp 82%smaller bytesresponsivemultiple widthsQueued jobs write derivatives next to originalRetry failed jobs from Horizon or queue:retry
Media library spatie conversions — thumb, WebP, and responsive variants from one upload

Ordering, replacing, and deleting

For ordered galleries:

$mediaItem->move($post, 'gallery', $newOrder);

Replace a single-file collection by uploading again. Spatie removes the previous file when singleFile() is set. Explicit delete:

$post->clearMediaCollection('featured');
$post->delete(); // cascades media rows; files removed via model event

When deleting models in bulk, watch disk I/O. For large cleanups I batch deletes and monitor queue backlog. That is the same discipline I apply during ongoing Laravel maintenance contracts.

How Do You Handle Production Storage, Queues, and Security?

Development on a local public disk hides problems. They surface the first time you deploy to AWS, DigitalOcean Spaces, or a shared host with limited inode counts.

S3 and CDN configuration

Point a collection at S3:

$this->addMediaCollection('gallery')
    ->useDisk('s3');

In config/filesystems.php, define the S3 disk per Laravel filesystem documentation. Set AWS_URL or a CloudFront domain so generated URLs point at your CDN, not raw bucket endpoints.

On document-heavy portals like client portals with file sharing, S3 with private ACL plus signed temporary URLs is the pattern. The media row exists in the database. The download route checks a policy before calling $media->getTemporaryUrl(now()->addMinutes(15)).

Security checklist

  • Validate file type and size server-side on every upload endpoint.
  • Never expose private collection files via public disk or predictable URLs.
  • Authorise upload, view, and delete with Laravel policies tied to model ownership.
  • Scan PDFs and office documents if users upload them — Spatie stores bytes; it does not antivirus-scan.
  • Store original filenames only for display; use hashed storage names to reduce path-guessing attacks.
  • Log failed conversion jobs — silent Imagick failures produce broken thumbnails in admin UIs.

Database and migration hygiene

The published migration creates a media table. Treat it like any core table: back it up, index foreign keys, and plan growth. On high-volume catalogues, the media table can grow faster than the parent model table. Pair with sensible retention rules and occasional orphaned-file audits.

If you version your schema carefully — as outlined in database migrations and seeding best practices — adding custom properties rarely needs new columns. Spatie stores them in JSON.

Storage DecisionWho accesses file?PublicRestrictedpublic disk + CDNproduct images, blog heroprivate S3 + policycontracts, ID scansqueue conversionsalways in productionbackup media tablewith nightly DB dump
Laravel spatie media — choosing public CDN vs private signed URLs

Real-world collection patterns

Patterns I reuse across custom Laravel applications:

DomainCollectionsConversionsDisk
eCommerce productimages, manualsthumb, webp, largeS3 public + private
Law firm portalclient-documents, avatarsthumb for avatars onlyprivate S3
Booking / trek agencyitinerary-pdf, gallerywebp, responsivemixed
User profileavatar (singleFile)thumb 150pxpublic

For JSON debugging of API payloads that include media metadata, a quick pass through the on-site JSON formatter saves time when mapping nested custom properties.

When building admin panels, Filament has community plugins that integrate Spatie Media Library. Useful if your team already standardised on Filament for back-office CRUD. The core lesson stays the same: collections and conversions live on the model, not scattered in admin form callbacks.

Key Takeaways

  • Implement HasMedia and InteractsWithMedia, then define collections in registerMediaCollections() before writing upload controllers.
  • Use named collections with MIME restrictions and singleFile() where only one asset belongs on a record.
  • Queue image conversions in production; reserve nonQueued() for tiny thumbnails only.
  • Match disk visibility to data sensitivity — public product images on CDN, client documents on private S3 with policy-gated signed URLs.
  • Pair Spatie with Form Request validation, Laravel policies, and responsive images for SEO-friendly, secure file handling.
  • Monitor the media table size and failed queue jobs — conversions fail quietly if Imagick is missing on the worker server.

People Also Ask

Does Spatie Media Library work with Laravel 13?

Yes. Install via Composer on Laravel 13 with PHP 8.3 or higher, publish migrations, and run php artisan migrate. The package tracks current Laravel releases. Laravel 12 is supported until February 2027. Laravel 11 reached EOL in March 2026 — upgrade if you still run that line.

Can Spatie Media Library store PDFs and documents, not just images?

Absolutely. Collections accept any MIME type you allow. Image conversions apply only to raster images. PDFs and spreadsheets store as-is. Use custom properties for titles or document types. Serve downloads through authorised controller routes rather than public URLs.

How do you regenerate image conversions after changing conversion settings?

Run php artisan media-library:regenerate, optionally filtered by model type or collection. On large catalogues, run it during a maintenance window or chunk models in a custom Artisan command. Imagick can spike CPU on a live web node if you regenerate everything at once.

Is Spatie Media Library better than storing files directly on the model?

For any model with more than one file type or any need for thumbnails and responsive images, yes. Path columns work for a single avatar on a hobby project. They become unmaintainable when you add galleries, document uploads, ordering, and cloud storage — exactly the problems this package was written to solve.

Ship File Uploads That Scale With Your Laravel App

You now have a production-oriented reference for spatie media library: install, model wiring, collections, conversions, cloud disks, and the security boundaries that separate public product photos from private client documents. On projects I deliver through web development in Nepal and remote engagements, this package is my default whenever uploads are more than a single column — especially on eCommerce builds and enterprise portals where files are operational data, not decoration.

If you want Spatie Media Library integrated into a new feature or an existing codebase upgraded to Laravel 13 with proper storage architecture, review the portfolio of shipped Laravel work and get in touch — include your current disk setup, expected upload volume, and whether files are public or confidential so the collection design starts in the right place.

Frequently Asked Questions

It is a Composer package that attaches one or many files to any Eloquent model. Metadata lives in a dedicated media table; file binaries sit on a configured disk such as local public storage or S3.

Yes. Require spatie/laravel-medialibrary with Composer 2.10 on Laravel 13 and PHP 8.3 or higher, publish the migration tag, then run php artisan migrate.

Yes. It is an open-source package with no license fee. You pay only for server hosting, optional S3-compatible storage, and queue infrastructure to process conversions.

Run composer require spatie/laravel-medialibrary, then php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-migrations" and php artisan migrate. Optionally publish medialibrary-config. Set FILESYSTEM_DISK and MEDIA_DISK in .env, run php artisan storage:link once per server, and configure QUEUE_CONNECTION to database or Redis 8.10 with a Supervisor-managed worker for production conversions.

Implement Spatie\MediaLibrary\HasMedia and use the InteractsWithMedia trait on the model. Define named collections in registerMediaCollections() with rules for single or multiple files, accepted MIME types, and disk. In your controller, call addMediaFromRequest() or addMedia() and chain usingFileName(), withCustomProperties(), and toMediaCollection() to attach validated uploads after the parent record is created.

Collections are named buckets on a model — for example featured, gallery, or documents — each with its own rules. You can restrict MIME types, allow only one file via singleFile(), and point sensitive uploads at a private S3 disk while keeping public product images on a CDN-backed disk. On legal-tech portals I have built, separating client documents from public assets prevented accidental exposure of PDFs stored under a public disk.

Register conversions in registerMediaConversions() on the model. Common patterns include a 300px cropped thumb with Fit::Crop, a WebP copy at quality 82 via performOnCollections(), and a queued 1200px preview. Use nonQueued() only for tiny thumbnails needed immediately; larger jobs should stay queued so HTTP requests do not wait on Imagick. For responsive srcsets, add withResponsiveImages() to a conversion and output via getFirstMedia()->img() in Blade.

Configure an s3 disk in config/filesystems.php with AWS credentials and region, then call useDisk('s3') on the collection in registerMediaCollections(). Set AWS_URL or a CloudFront domain so generated URLs hit your CDN rather than raw bucket endpoints. Development on a local public disk hides inode and scaling issues that appear the first time you deploy to AWS or DigitalOcean Spaces, so test S3 uploads before launch.

Store sensitive collections on a private S3 disk, never on storage/app/public. Authorise upload, view, and delete through Laravel policies tied to model ownership. For downloads, check the policy in a dedicated route, then return a signed temporary URL via getTemporaryUrl(now()->addMinutes(15)). Validate MIME type and file size server-side in a Form Request on every upload endpoint, and use hashed storage names instead of original filenames to reduce path-guessing attacks.

Path columns such as photo_path are fast to prototype but break with multiple files, offer no built-in conversions, and push disk logic into controllers. A DIY polymorphic attachments table is flexible but you rebuild ordering, responsive images, and conversion queues yourself. Spatie bundles collections, conversions, queues, and responsive images in one package aligned with fat-model Laravel architecture — at the cost of a learning curve and a required queue worker for async conversions.

Yes, in production. Conversions run queued by default, which is what you want for uploads larger than a small thumbnail. Set QUEUE_CONNECTION to database or Redis 8.10 and run a worker via Supervisor. Reserve nonQueued() only for synchronously needed tiny thumbs. On Deployer-managed releases, restart queue workers after deploy alongside migrate, symlink, and PHP-FPM reload — the same pattern I use on GitLab CI pipelines for Laravel apps.

Image conversions require either GD or Imagick. On Ubuntu servers I maintain, Imagick is preferred for quality and memory behaviour — install php8.5-imagick and reload PHP-FPM. If Imagick is missing on the queue worker server, conversion jobs fail quietly and admin UIs show broken thumbnails, so log failed conversion jobs and verify the extension on both web and worker processes.

Use getFirstMediaUrl('collection', 'conversion') for a single image URL, passing a conversion name like thumb or webp as the second argument. getFirstMedia('collection') returns the Media model for custom properties such as alt text stored via withCustomProperties(). Loop galleries with getMedia('collection') and guard rendering with hasMedia('collection'). For API responses, map media in an API resource rather than exposing raw disk paths, keeping authorisation at the policy layer.

Always validate in a Form Request, never relying on client-side MIME checks alone. Typical rules include image and max size limits for photos, mimes:pdf for documents, and per-item rules for gallery arrays such as gallery.*. Spatie collection MIME restrictions via acceptsMimeTypes() add a second layer, but server-side Laravel validation remains mandatory on every upload endpoint before calling addMediaFromRequest() or addMedia().

The most common cause is Imagick or GD missing on the queue worker server even though it is installed on the web PHP-FPM pool. Failed jobs leave originals intact but produce broken thumb or webp URLs in admin panels. Check your queue logs, confirm php8.5-imagick is installed on worker hosts, and ensure Supervisor restarts workers after deployment. Also verify QUEUE_CONNECTION is set and the worker process is actually running — not just configured in .env.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: