File Uploads
The File field uploads to temporary storage by default. That is the path most forms should use: the user gets immediate progress and validation, while the final Inertia request only submits a small encrypted token.
Choosing an Upload Path
Start with the default temporary upload. Reach for the other methods only when the form needs something specific:
- Use
File::make('avatar')for normal uploads, previews, edit forms, and Spatie Media Library fields. - Use
storeWithForm()when a small file should be sent with the final form request as a native multipartFile. - Use
temporaryUploadUrl('/uploads/avatar')when your app already has a temporary upload endpoint. - Use
chunked()for large same-origin uploads that need pause, resume, retry, or smaller requests. - Use
directToStorage('s3')ordirectToStorage('minio')when the browser should send bytes directly to a Laravel filesystem disk backed by S3-compatible object storage.
Multiple files work with each path. Temporary uploads, chunked uploads, and direct-to-storage uploads all submit encrypted temporary upload tokens in the current UI order.
Choose one upload transport per field. Direct-to-storage takes precedence in the frontend when both chunked() and directToStorage() are enabled.
| Destination | Transport | API | Pause/resume |
|---|---|---|---|
| Laravel server | Final form multipart request | storeWithForm() | No |
| Laravel server | Temporary upload before submit | Default File::make() | No |
| Laravel server | Temporary upload before submit, split into chunks | chunked() | Yes |
| S3-compatible object storage | Single signed upload below the multipart threshold | directToStorage() | No |
| S3-compatible object storage | Multipart upload above the multipart threshold | directToStorage() | Yes |
Temporary Uploads
Temporary uploads are the default:
File::make('avatar');File selection follows this flow:
- The frontend field posts the file to
POST /_inertia-forms/file-upload. - Laravel stores the file temporarily and validates upload-time file rules.
- The endpoint returns JSON with a
key. - The form stores that key instead of the raw browser file.
- A
FormRequestimplementingHasUploads, or an explicitHandleFileUploads::forRequest()call, decrypts the key and exposes the resolved upload as aSubmittedUploadthrough request macros.
The endpoint response looks like this:
{
"key": "encrypted-temporary-upload-token",
"name": "avatar.jpg",
"mimeType": "image/jpeg",
"mime_type": "image/jpeg",
"size": 12345
}The key value is the submitted form value. For multiple fields, the value is an array of keys.
Reading Submitted Uploads
Temporary upload handling does not replace $request->input() or $request->all() values with raw UploadedFile instances. The submitted input still contains the encrypted key, while the resolved files are stored separately on the request.
The #[Validate] attribute validates a generated form class, but it does not hydrate temporary upload tokens. Controllers that need SubmittedUpload objects may hydrate the request explicitly, or move those rules to a FormRequest implementing HasUploads.
A plain Request must be hydrated before these macros are read. Pass every single or nested upload key to HandleFileUploads::forRequest(), then use formUpload() for one file and orderedFormUploads() for an ordered collection:
use Illuminate\Http\Request;
use InertiaUI\Forms\FileUploads\Http\Middleware\HandleFileUploads;
use InertiaUI\Forms\FileUploads\SubmittedUpload;
public function store(Request $request)
{
HandleFileUploads::forRequest($request, ['avatar', 'photos']);
$avatar = $request->formUpload('avatar'); // ?SubmittedUpload
if ($uploadedFile = $avatar?->getUploadedFile()) {
$originalName = $uploadedFile->getClientOriginalName();
}
$photos = $request->orderedFormUploads('photos')
->map(fn (SubmittedUpload $photo) => $photo->getIdentifier());
}For automatic hydration, implement the marker contract on a FormRequest. The package reads that request's rules, discovers file and temporary-token paths, and hydrates it when Laravel resolves the request:
use Illuminate\Foundation\Http\FormRequest;
use InertiaUI\Forms\Fields\File;
use InertiaUI\Forms\FileUploads\Contracts\HasUploads;
class StoreProfileRequest extends FormRequest implements HasUploads
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
$avatar = File::make('avatar')->image()->nullable();
$photos = File::make('photos')->multiple()->image()->nullable();
return [
'avatar' => $avatar->getRules(),
...$avatar->getAdditionalRules(),
'photos' => $photos->getRules(),
...$photos->getAdditionalRules(),
];
}
}SubmittedUpload may represent a new temporary upload, a direct-to-storage upload, or an existing file that stayed selected while editing. Use isNew() and isExisting() to branch, getUploadedFile() for a new local or remote upload, getRemoteFile() when direct storage details matter, getExistingFile() for a retained existing value, and getIdentifier() for a stable order key. For fields bound to Spatie Media Library, prefer HandleFileUploads::syncMediaLibrary().
Store With The Form
Use storeWithForm() when you want a newly selected file to travel with the final Inertia form request:
File::make('document')->storeWithForm();This is useful for small create forms where you do not need existing file preservation, preview tokens, or Media Library synchronization. In this mode, Laravel receives native multipart File values during the normal form submission and all validation runs at submit time.
Custom Temporary Endpoint
Use temporaryUploadUrl() when the browser should post selected files to your own endpoint:
File::make('avatar')->temporaryUploadUrl('/uploads/avatar');Your endpoint receives multipart data with the file in the file field. Return a submitted value in key, path, url, or id; key is recommended because it matches the built-in endpoint.
Custom endpoints that return Inertia Forms temporary upload tokens for fields with upload-time rules must include the expected validation hash. Endpoints that perform equivalent validation themselves should opt out of that token guard:
File::make('avatar')
->temporaryUploadUrl('/uploads/avatar')
->requireValidatedUploads(false);Chunked Uploads
Use chunked() for large files that should still flow through your Laravel app, but should not depend on one huge request:
File::make('video')
->chunked(size: 8 * 1024 * 1024); // bytesThe browser starts a pending upload, appends chunks in order, checks status when needed, and completes it into the same encrypted temporary upload token used by regular temporary uploads. The built-in UI may pause, resume, cancel, retry, and show byte-based progress.
Chunked uploads are the right fit for local 1-2 GB uploads when PHP is still responsible for receiving the bytes.
Direct To Storage
Use directToStorage() when the browser should send bytes to a Laravel filesystem disk instead of proxying them through PHP. This follows Laravel's Storage mental model: configure a disk in config/filesystems.php, then tell the field which disk should receive the upload.
File::make('archive')
->directToStorage('s3')
->partSize(16 * 1024 * 1024)
->multipartThreshold(100 * 1024 * 1024);Laravel still controls the upload. It creates the signed URLs, validates the completed object, runs custom validators, returns the encrypted temporary upload token, and later exposes that token as a SubmittedUpload during form submission. The difference is that the heavy file bytes go straight from the browser to S3, MinIO, or another S3-compatible disk.
Files at or below the multipart threshold use one signed PUT. Larger files use S3 multipart upload parts and may resume from already uploaded parts.
The argument passed to directToStorage() is a Laravel disk name from config/filesystems.php, just like Storage::disk('s3'). Most applications only need to configure a normal S3-compatible disk and pass that disk name to the field.
S3 Configuration
Install Laravel's S3 filesystem adapter in the consuming app:
composer require league/flysystem-aws-s3-v3Configure a normal Laravel s3 disk in config/filesystems.php, then point Inertia Forms at that disk:
// config/inertia-forms.php
'file_uploads' => [
'direct_to_storage' => [
'disk' => 's3',
'url_lifetime' => 900,
'part_size' => 16 * 1024 * 1024,
'multipart_threshold' => 100 * 1024 * 1024,
],
],Publish the runtime config from Installation before editing these values.
Your bucket CORS policy must allow browser PUT requests from your app origin and expose ETag so multipart completion may send the uploaded part identifiers back to Laravel.
Minimum CORS shape:
[
{
"AllowedOrigins": ["https://your-app.example"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]MinIO
MinIO uses Laravel's normal s3 filesystem driver. Define it as a normal Laravel filesystem disk:
// config/filesystems.php
'minio' => [
'driver' => 's3',
'key' => env('MINIO_ACCESS_KEY_ID', 'minioadmin'),
'secret' => env('MINIO_SECRET_ACCESS_KEY', 'minioadmin'),
'region' => env('MINIO_REGION', 'us-east-1'),
'bucket' => env('MINIO_BUCKET', 'inertia-forms-tests'),
'endpoint' => env('MINIO_ENDPOINT', 'http://127.0.0.1:9000'),
'use_path_style_endpoint' => true,
'throw' => true,
],Then use that disk globally or per field:
File::make('archive')->directToStorage('minio');Local MinIO setups may configure browser CORS globally with MINIO_API_CORS_ALLOW_ORIGIN instead of accepting S3 bucket CORS through PutBucketCors. The browser requirement is the same: the signed PUT requests must be allowed from your app origin and multipart responses must expose ETag.
Routes
Register the upload routes in routes/web.php:
Route::inertiaFormUploads();This registers the built-in endpoints under /_inertia-forms by default. You usually do not need to call these routes directly; the frontend field reads the route names from the serialized field configuration.
Temporary uploads use two endpoints with the default route-name prefix:
POST /_inertia-forms/file-upload(inertia-forms.file-upload.store) stores a selected file and returns an encrypted temporary upload token.DELETE /_inertia-forms/file-upload(inertia-forms.file-upload.destroy) removes an unused temporary upload.
Chunked uploads add a small route group below /_inertia-forms/file-upload/chunked:
startcreates a pending upload.statuschecks which bytes have already been received.chunkappends the next chunk.completeturns the uploaded chunks into a temporary upload token.abortcancels the pending upload.
Direct-to-storage uploads add a route group below /_inertia-forms/file-upload/direct:
startcreates a signed single-object or multipart upload.objectreceives local test-driverPUTuploads.partsigns or receives individual multipart parts.statuschecks already uploaded parts.completevalidates the stored object and returns a temporary upload token.abortcancels an unfinished upload.
You may customize the prefix and middleware:
Route::inertiaFormUploads(
prefix: '/uploads',
middleware: ['web', 'auth', 'throttle:60,1'],
);For separate application sections, register another route group with its own route-name prefix:
Route::prefix('admin')
->name('admin.')
->group(function () {
Route::inertiaFormUploads(
prefix: '/backoffice/_inertia-forms',
middleware: ['web', 'auth', 'can:access-admin'],
name: 'backoffice',
);
});Use uploadRoutes() on fields that should use that route group. The selected route group supplies the temporary, delete, chunked, and direct-to-storage endpoints for the field:
File::make('avatar')
->uploadRoutes('admin.backoffice');Pass the full route-name prefix after Laravel composes parent route groups and the upload route group name.
Serialized Upload Configuration
PHP serializes upload methods into the same public props used by true standalone Vue and React controls. Generated fields fill the route URLs and validation token automatically. Standalone controls must provide the values required by their chosen transport.
| Prop | Meaning |
|---|---|
storeWithForm | Keep native browser File objects for the final multipart form request. It bypasses temporary, chunked, and direct uploaders. |
temporaryUploadUrl | Regular multipart upload endpoint. The request field is file; the response may supply the submitted value as key, path, url, or id. |
temporaryUploadDeleteUrl | Optional delete endpoint used to discard a completed temporary upload that the user removes. |
chunked / chunkSize | Enable same-origin chunking and set bytes per append request. |
chunkedUrls | Endpoints named start, status, append, complete, and abort; all are required for standalone chunked uploads. |
directToStorage / uploadDisk | Enable direct upload and identify the Laravel filesystem disk sent to the signing endpoint. |
uploadPartSize / uploadMultipartThreshold | Set multipart part bytes and the byte threshold above which direct uploads become multipart. |
directUploadUrls | Endpoints named start, signPart, status, complete, and abort; all are required for standalone direct uploads. |
uploadRulesToken | Encrypted package validation profile sent with managed upload requests. Applications normally obtain it from serialized PHP props. |
requiresUploadRulesToken | Require the completed temporary token to prove it used the expected validation profile in the final request. |
The FileUpload field also accepts existingFiles as an explicit initial list. A non-empty list takes precedence over existing-file objects in the initial bound value. Composer applies the same transport props to attachments. RichText nests them under imageUploads and never supports storeWithForm.
Configuration
The upload configuration is grouped by responsibility:
'file_uploads' => [
'route_prefix' => '/_inertia-forms',
'route_name' => 'inertia-forms.',
'middleware' => ['web', 'auth'],
'temporary_uploads' => [
'disk' => '',
'lifetime' => 3600,
'max_size' => 10240,
'chunked' => [
'size' => 5 * 1024 * 1024,
'max_size' => 2 * 1024 * 1024,
],
],
'direct_to_storage' => [
'disk' => '',
'url_lifetime' => 900,
'part_size' => 16 * 1024 * 1024,
'multipart_threshold' => 100 * 1024 * 1024,
'max_size' => 5 * 1024 * 1024,
],
],Leave temporary_uploads.disk empty to let Inertia Forms create a local temporary upload staging disk at storage/inertia-forms-temporary-uploads. Set it to any disk in config/filesystems.php when temporary upload tokens should stage files elsewhere. This disk is not the final destination for submitted files.
Validation
File fields validate in two phases.
Upload-time rules are added by file-specific methods such as image(), maxSize(), accept(), and dimensions(). They run while Laravel has access to a real uploaded file:
File::make('avatar')
->image()
->maxSize(2048)
->maxDimensions(width: 1000, height: 1000);Submit-time rules such as required() run when the form is submitted:
File::make('avatar')
->required()
->image()
->maxSize(2048);A submitted temporary token strips file-specific rules from submit-time validation because the submitted input contains an encrypted string. Use formUpload() or orderedFormUploads() when controller code needs the resolved SubmittedUpload.
Managed package uploads also bind the upload validation profile to the returned temporary token. Form validation rejects unvalidated temporary tokens submitted to managed fields with upload rules.
That token guard is automatic for the built-in temporary route, chunked uploads, and direct-to-storage uploads. A custom temporaryUploadUrl() defaults to no package-token guard because the response may be an application-owned path, url, or id. Use requireValidatedUploads(true) only when that custom endpoint returns compatible Inertia Forms tokens; use false to disable the guard explicitly.
Upload Validators
Use validateUploadsUsing() for malware scans, content validation, tenant policy, or other upload-time validation that does not fit Laravel's built-in file rules:
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\ValidationException;
use InertiaUI\Forms\FileUploads\Contracts\ValidatesFileUploads;
use InertiaUI\Forms\FileUploads\TemporaryUpload;
use InertiaUI\Forms\FileUploads\UploadRules;
class EnsureArchiveIsSafe implements ValidatesFileUploads
{
public function validate(
TemporaryUpload $upload,
UploadedFile $file,
UploadRules $rules,
Request $request,
): void {
if ($file->getClientOriginalExtension() !== 'zip') {
return;
}
// Run your scanner here...
$blocked = false;
if ($blocked) {
throw ValidationException::withMessages([
'file' => ['The archive failed the security scan.'],
]);
}
}
}
File::make('archive')
->directToStorage('s3')
->maxSize(1024 * 1024)
->validateUploadsUsing(EnsureArchiveIsSafe::class);Upload validators run after the completed object is available and before the encrypted temporary token is returned to the browser. Throw a ValidationException to return a normal 422 upload error and clean up the rejected temporary object.
For direct-to-storage uploads, Laravel streams remote S3 or MinIO objects to a temporary local file while validating so Laravel file rules and custom validators may read the completed bytes.
Spatie Media Library
For Spatie Media Library, bind a file field to a media collection:
File::make('photos')
->multiple()
->mediaCollection('photos');Edit forms automatically load existing media as preview items. On submit, use HandleFileUploads::syncMediaLibrary() to add new uploads, remove deleted items, and preserve the submitted order:
public function update(Request $request, Product $product)
{
HandleFileUploads::syncMediaLibrary(
request: $request,
model: $product,
key: 'photos',
collectionName: 'photos',
);
}Pass the optional disk argument to override the media collection's storage disk. Leave it empty to use the collection's configured disk.
Existing Files
Edit forms may pass existing files to the form so they appear in the UI.
From a storage disk. A string path returns one ExistingFile; an array of paths returns an array. Image previews use a temporary URL with a five-minute default expiration. Pass the optional expiration, withPreview, and metadata arguments when a different behavior is required:
use InertiaUI\Forms\FileUploads\ExistingFile;
$existingFile = ExistingFile::fromDisk('public', 'avatars/photo.jpg');
$withoutPreview = ExistingFile::fromDisk('public', 'documents/report.pdf', withPreview: false);ExistingFile::fromFilesystem() accepts an already-resolved Laravel filesystem instance with the same path, preview, and metadata options.
From Media Library:
$existingFiles = ExistingFile::fromMediaLibrary($product->getMedia('photos'));Use fromMediaLibraryWithoutPreview() when the media items should remain selectable without generating preview URLs. fromMediaLibrary() accepts one media model or an iterable and uses a five-minute preview expiration unless you pass another value.
Existing files serialize to a browser value with an encrypted key plus display fields:
{
"key": "encrypted-existing-file-token",
"id": "stable-file-identifier",
"identifier": "stable-file-identifier",
"filename": "photo.jpg",
"name": "photo",
"previewUrl": "https://example.com/photo.jpg",
"preview_url": "https://example.com/photo.jpg",
"mimeType": "image/jpeg",
"mime_type": "image/jpeg",
"size": 12345,
"size_in_bytes": 12345
}Selected existing files submit their key. Removed files omit that key from the submitted value.
Cleanup
Temporary uploads that are never submitted accumulate on disk. Schedule the cleanup command to remove expired files:
// In app/Console/Kernel.php or routes/console.php
Schedule::command('form:cleanup-uploads')->hourly();The command deletes temporary upload directories older than file_uploads.temporary_uploads.lifetime, which defaults to one hour:
php artisan form:cleanup-uploads --lifetime=7200For S3-compatible direct-to-storage uploads, also configure bucket lifecycle rules to expire abandoned inertia-forms-upload-* objects and abort incomplete multipart uploads. Multipart sessions that are never completed are not visible as ordinary objects until S3 completes them.
For large-upload testing, keep normal CI focused on representative multipart uploads. The demo browser suite uses a live MinIO multipart upload that is large enough to exercise multipart behavior without making every pull request move gigabytes of data. Reserve 1-2 GB upload tests for manual or scheduled infrastructure smoke runs.