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 look simple until your app stores client contracts, product galleries, PDF receipts, and profile photos on the same model. The Laravel Spatie Media Library Complete Guide you are reading walks through Spatie Laravel Media Library end to end — from Composer install on Laravel 13 through collections, conversions, cloud disks, and the production gotchas I hit on legal-tech portals and eCommerce builds. If you already ship Laravel apps and want one package to attach, transform, and serve files without reinventing upload logic, this is the reference page.

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

Spatie Laravel Media Library is a first-party-quality package that associates one or many files with any Eloquent model. Instead of scattering avatar_path, thumbnail, and document_url columns across tables, you keep file metadata in a dedicated media table and store binaries on a configured filesystem disk.

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 solves 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 Library ArchitectureEloquent ModelHasMedia traitmedia tablemetadata + disk pathStorage Disklocal / S3 / publicCollectionsConversionsCustom propsOne package replaces ad-hoc file columns and manual resize scripts
Laravel Spatie Media Library complete guide — model, database metadata, and filesystem storage

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

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; requires queue worker 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 version in my stack. Composer 2.10 handles the install. The package supports Laravel 11 through 13 — check the release notes if you are still on Laravel 12 (supported until February 2027).

Install the package and publish assets

  1. Require the package:
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 either GD or Imagick. On Ubuntu servers I maintain, Imagick is preferred for 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 is fine 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

For queued conversions — which you want on any upload larger than a thumbnail — set QUEUE_CONNECTION=database or Redis 8.10 and run a worker via Supervisor. I use the same pattern on Deployer-managed releases described in my GitLab CI pipeline for Laravel notes: migrate, symlink, reload PHP-FPM, restart queue workers.

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

The heart of this Laravel Spatie Media Library complete guide 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 form request with validation:

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 API endpoints returning JSON, map media in an API resource rather than exposing raw disk paths. That mirrors patterns in Laravel API best practices and keeps 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 written to diskcustom properties stored as JSONQueue: conversionsBlade / API URL
End-to-end 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 — a mistake I have seen when teams store everything under storage/app/public without thinking through authorisation.

How Do You Create Image Conversions and Responsive Variants?

Conversions are derived files generated 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 synchronously on upload — otherwise your HTTP request waits on Imagick.

For deeper image tuning, pair this package with the dedicated walkthrough on image optimization in Laravel with Spatie Media Library and front-end lazy loading 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 — something I treat as architecture, not a post-launch patch, when doing technical SEO work alongside development. Structured product images on florist eCommerce builds benefit directly from WebP conversions plus responsive srcsets.

Image Conversion PipelineOriginal UploadJPEG 4000pxthumb 300pxcropped squarewebp 82%smaller bytesresponsivemultiple widthsQueued jobs write derivatives next to original on diskFailed jobs leave original intact — retry from Horizon or artisan queue:retry
Spatie media 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 — the same operational 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 that appear the first time you deploy to AWS, DigitalOcean Spaces, or a client's 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 digital product platforms and 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, but 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 other 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 because Spatie stores them in JSON.

Production 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 Library complete guide — 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; if you remain on Laravel 12, it is supported until February 2027 — plan upgrades before Laravel 11's EOL in March 2026 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, and 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 so Imagick does not spike CPU on a live web node.

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 Laravel Spatie Media Library Complete Guide: 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: