sarkis-sh/spray-media
Spray Media is a Laravel package for managing uploaded media and attaching files to Eloquent models. It offers a simple foundation for storing, retrieving, and organizing images and documents, with configurable storage and straightforward model integration.
serving.mode = redirect moves file egress off PHP entirely — the app only validates signature/expiry and returns a 302. Combined with direct uploads, the application tier stops carrying file bytes in either direction. This is the difference between "scale the app servers" and "scale the bucket/CDN," and it makes bandwidth a storage-vendor line item rather than a compute line item. Pairing with Cloudflare R2 (zero egress) is an explicit, supported path and materially changes the unit economics of a media-heavy product.DirectUploadPolicyInterface is the commercially interesting piece: a policy can refuse an upload, choose the destination folder, attach attributes to the record, and apply per-kind size and type limits. That maps directly onto free/pro/enterprise quotas, per-workspace storage pools, and feature-specific constraints (e.g. avatars capped at 2 MB, attachments at 50 MB, video at 2 GB) without forking the upload endpoint.spray-media:prune-pending cleans up never-completed uploads on an hourly schedule. Without this, abandoned direct uploads silently accumulate as paid storage; with it, orphan cost is bounded and predictable.Adopt when most of the following hold:
league/flysystem-aws-s3-v3) but is otherwise undemanding.Look elsewhere when:
For executives — one paragraph. This release lets us take file uploads and downloads off our own servers. Files go directly from the user's device into cloud storage, and downloads redirect to short-lived storage links, so our application servers no longer carry file traffic in either direction. The practical effects are lower and more predictable infrastructure cost (especially paired with zero-egress storage like Cloudflare R2), faster and more reliable uploads for customers on weak connections, and a stronger security posture because every upload is verified against its declared size and actual file type before we accept it. It also gives us the mechanism to enforce storage limits by plan tier, which is a prerequisite for packaging storage as a paid feature. Critically, this is low-risk to adopt: it ships switched off, requires no database migration, and does not break anything currently in production, so we can turn it on incrementally and measure.
For engineering — the short version. Direct-to-disk uploads in three calls (initiate → PUT to signed URL → complete), with completion validating real byte size and real MIME type from magic bytes, deleting failures, and writing nothing to the database until step 3 passes. Tokens are encrypted and single-use. DirectUploadPolicyInterface and bindings.upload_url_signer are the two swap points if we need custom admission rules or a different signing scheme — a token only completes through the policy that issued it. Redirect serving keeps Content-Type and inline/attachment disposition. New env flags (SPRAY_MEDIA_DISK, SPRAY_MEDIA_SERVE_MODE, SPRAY_MEDIA_DIRECT_UPLOADS) are off by default; published configs only need new bindings keys if we intend to override, and the serving / direct_uploads defaults come from the package. Needs league/flysystem-aws-s3-v3 and an S3 disk on Laravel 11+. Action item: schedule spray-media:prune-pending hourly before enabling direct uploads — that is the one operational step that is easy to forget and costly to skip.
For design and support — what changes for users. Uploads get faster and less failure-prone on large files and poor networks, with no UI change strictly required. If we enable per-plan limits through a policy, users will start seeing clear, server-enforced reasons a file was rejected rather than silent failures — worth designing error states for.
How can I help you explore Laravel packages today?