Laravel filesystem, uploads, and object storage
Status: Complete. Last reviewed 2026-08-27.
Precise mental model
Section titled “Precise mental model”Laravel’s filesystem abstraction maps a named disk to a Flysystem adapter and configuration. It gives application code common operations for paths, streams, visibility, URLs, and temporary URLs; it does not make a local filesystem and an object store share filesystem semantics. A disk determines durability, visibility, consistency behavior, URL generation, credentials, and which processes can see an object.
The local driver’s root is a directory on one machine. The public disk conventionally maps to storage/app/public and a symbolic link exposes it beneath public/storage. In containers or horizontally scaled hosts, local writes disappear with the instance or are invisible to another instance unless backed by durable shared storage. S3-compatible disks store objects addressed by keys. “Folders” are key prefixes, renames are generally copy-and-delete operations, and append or random in-place mutation is not a safe portability assumption.
Upload boundary and object identity
Section titled “Upload boundary and object identity”An uploaded file is untrusted input even after Laravel validates it. Client filenames, extensions, and media types are hints supplied through several layers; validate size and allowed content, then independently inspect or transform high-risk formats. Never construct a storage path directly from a client filename. Laravel’s upload helpers can generate a unique name, but the application should still own a stable object identity in the database.
A robust flow creates an attachment record with tenant, owner, status, intended media class, and generated object key. Stream the upload to a private disk so PHP does not load the entire body into memory. Record returned paths instead of reconstructing them later. If malware scanning or media processing is required, keep the object quarantined and unavailable until an asynchronous job records a clean result. Authorization is evaluated against the attachment record, not inferred from an unguessable key.
Database and object-store writes cannot normally share one transaction. If the database commit succeeds and upload fails, the record is incomplete; if upload succeeds and the transaction rolls back, the object is orphaned. Model explicit states such as pending, available, and failed, use stable operation IDs, and run reconciliation that removes abandoned uploads and reports records whose objects are missing. A direct-to-store upload issues a narrowly scoped presigned request, then the application verifies object existence, size, checksum, and ownership before marking it available.
Visibility, URLs, and authorization
Section titled “Visibility, URLs, and authorization”Flysystem visibility is a portable vocabulary, usually private or public, mapped to adapter-specific permissions or ACL behavior. It is not application authorization. Public visibility means anyone with the URL may be able to read the object. Prefer private objects and expose them through an authorized download controller or a short-lived temporary URL.
An application-proxied download can apply a policy on every request, set a safe Content-Disposition, and log access, but it consumes application bandwidth. A temporary URL lets the store deliver efficiently, yet the URL is a bearer capability until expiry and can be shared or logged. Keep lifetimes short, avoid placing sensitive query strings in analytics, and decide whether immediate revocation is required. CDN caches add another revocation and freshness layer.
Storage::url() generates a URL according to disk configuration; it does not prove the object is public or reachable. temporaryUrl() depends on driver support and signer configuration. Laravel can build temporary URLs for a disk when a custom mechanism is needed. Test URLs against the deployed proxy, CDN, and store topology, not only with Storage::fake().
Names, overwrite, and integrity
Section titled “Names, overwrite, and integrity”Treat object keys as identifiers, not display names. A useful scheme includes environment and tenant boundaries, a generated attachment identifier, and perhaps a derivative class. Preserve the original name as metadata after stripping control characters, path separators, and unsafe response-header content.
Many writes overwrite an existing key. Generate collision-resistant keys and use database uniqueness or conditional store operations where overwriting violates an invariant. Store size, detected content type, and a cryptographic checksum when integrity or deduplication matters. For large direct uploads, multipart completion can be retried; make completion idempotent and clean up abandoned multipart sessions.
Deletion is also a workflow. Removing a database row first can orphan data; deleting the object first can leave a live record that points nowhere. For regulated retention, soft deletion and delayed purge may be required. Queue deletion after a committed state transition, make repeated deletion harmless, and reconcile both directions. Object versioning and lifecycle rules can improve recovery but may conflict with legal erasure unless explicitly governed.
Failure modes and testing
Section titled “Failure modes and testing”Expect timeouts, partial reads, permissions drift, expired credentials, DNS/TLS failures, throttling, missing buckets, region mistakes, and successful writes followed by a lost response. Bound client timeouts and retries; use a stable key so retry does not create another logical attachment. Do not catch every storage exception and report success. Surface a retryable state, record operation context, and monitor latency/error rate, object growth, failed scans, abandoned uploads, and reconciliation mismatches.
Storage::fake() is excellent for proving intended disk, path, contents, and absence. It does not prove IAM permissions, bucket policy, encryption, presigning, CORS, multipart behavior, CDN headers, or production limits. Retain a small integration suite against an isolated real bucket or compatible service and an operational smoke test using deployment credentials.
Current and legacy context
Section titled “Current and legacy context”Current: Laravel 13 exposes Flysystem-backed local, SFTP, S3-compatible, scoped, and read-only disks, streamed uploads, temporary URLs, and temporary upload URLs for supported drivers. Common: Laravel 11–12 use substantially the same disk model. Legacy: applications may write directly into public/, trust client filenames, or assume a shared server disk. Migrate by introducing stable metadata and private delivery before moving bytes.
Interview practice
Section titled “Interview practice”- LARAVEL-FILES-01 — Design a secure upload pipeline
- LARAVEL-FILES-02 — Choose public delivery, proxying, or temporary URLs
- LARAVEL-FILES-03 — Reconcile database and object-store state
- LARAVEL-FILES-04 — Explain why disks are not interchangeable filesystems
- LARAVEL-FILES-05 — Test an object-storage integration honestly