Attachments and media
filament/spatie-laravel-media-library-plugin delivers the media layer — uploads, collections and conversions — inside Filament’s form, table and infolist components. The demo model App\Models\Projeto shows the whole design:
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class Projeto extends Model implements HasMedia
{
use InteractsWithMedia;
public function registerMediaCollections(): void
{
$this->addMediaCollection('anexos');
}
public function registerMediaConversions(?Media $media = null): void
{
$this->addMediaConversion('miniatura')
->nonQueued() // with no guaranteed worker running: queued, the
->width(200) // conversion would only exist with a worker up, and the
->height(200); // column would stay empty with no error at all
}
}
And ProjetoResource consumes both ends:
SpatieMediaLibraryFileUpload::make('anexos'), // in the form
SpatieMediaLibraryImageColumn::make('anexos') // in the table
->simpleLightbox(),
->simpleLightbox() works with no glue because SpatieMediaLibraryImageColumn extends ImageColumn, which is exactly where the lightbox macro is registered.
Look at the thumbnails stacked on the record’s row: each one is served through a signed URL, because the disk is private — the same file requested without the signature answers 403.
Organization scoping comes for free — and that’s the point. Spatie’s media table is polymorphic: the file belongs to the record, and the record is already scoped by BelongsToTenant. Whoever can’t reach the project can’t reach the attachment, with no tenant column in media and no configuration to remember to turn on.
⚠️ The default media disk is
local, and it is private on purpose. WithMEDIA_DISK=publicthe file lands instorage/app/public, served by thepublic/storagesymlink: path/storage/{id}/{file}, sequential ID, reachable without a session — Filament’s multi-tenancy does not reach the file system. Usepubliconly for avatars and logos, which show up on the login screen.Two practical consequences of the private disk:
Media::getUrl()answers 403. That is fail-closed, and it is what you want. To publish a link to private media usegetTemporaryUrl(), which signs the URL.- Whoever holds the link gets in, for as long as the signature is valid, with no session. Laravel’s
storage.localroute validates the signature, not the user: sharing the link shares the file until it expires. For attachments that need per-organization authorization, serve them through your own route that checks the policy first.Already running an install with
MEDIA_DISK=public? The new config only protects NEW files. Runphp artisan kit:midia-privada(it takes--dry-run) to move what was already written — without it, the old media stays served by the symlink.