Skip to content

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 multipart File.
  • 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') or directToStorage('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.

DestinationTransportAPIPause/resume
Laravel serverFinal form multipart requeststoreWithForm()No
Laravel serverTemporary upload before submitDefault File::make()No
Laravel serverTemporary upload before submit, split into chunkschunked()Yes
S3-compatible object storageSingle signed upload below the multipart thresholddirectToStorage()No
S3-compatible object storageMultipart upload above the multipart thresholddirectToStorage()Yes

Temporary Uploads

Temporary uploads are the default:

php
File::make('avatar');

File selection follows this flow:

  1. The frontend field posts the file to POST /_inertia-forms/file-upload.
  2. Laravel stores the file temporarily and validates upload-time file rules.
  3. The endpoint returns JSON with a key.
  4. The form stores that key instead of the raw browser file.
  5. A FormRequest implementing HasUploads, or an explicit HandleFileUploads::forRequest() call, decrypts the key and exposes the resolved upload as a SubmittedUpload through request macros.

The endpoint response looks like this:

json
{
    "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:

php
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:

php
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:

php
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:

php
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:

php
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:

php
File::make('video')
    ->chunked(size: 8 * 1024 * 1024); // bytes

The 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.

php
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:

bash
composer require league/flysystem-aws-s3-v3

Configure a normal Laravel s3 disk in config/filesystems.php, then point Inertia Forms at that disk:

php
// 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:

json
[
    {
        "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:

php
// 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:

php
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:

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:

  • start creates a pending upload.
  • status checks which bytes have already been received.
  • chunk appends the next chunk.
  • complete turns the uploaded chunks into a temporary upload token.
  • abort cancels the pending upload.

Direct-to-storage uploads add a route group below /_inertia-forms/file-upload/direct:

  • start creates a signed single-object or multipart upload.
  • object receives local test-driver PUT uploads.
  • part signs or receives individual multipart parts.
  • status checks already uploaded parts.
  • complete validates the stored object and returns a temporary upload token.
  • abort cancels an unfinished upload.

You may customize the prefix and middleware:

php
Route::inertiaFormUploads(
    prefix: '/uploads',
    middleware: ['web', 'auth', 'throttle:60,1'],
);

For separate application sections, register another route group with its own route-name prefix:

php
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:

php
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.

PropMeaning
storeWithFormKeep native browser File objects for the final multipart form request. It bypasses temporary, chunked, and direct uploaders.
temporaryUploadUrlRegular multipart upload endpoint. The request field is file; the response may supply the submitted value as key, path, url, or id.
temporaryUploadDeleteUrlOptional delete endpoint used to discard a completed temporary upload that the user removes.
chunked / chunkSizeEnable same-origin chunking and set bytes per append request.
chunkedUrlsEndpoints named start, status, append, complete, and abort; all are required for standalone chunked uploads.
directToStorage / uploadDiskEnable direct upload and identify the Laravel filesystem disk sent to the signing endpoint.
uploadPartSize / uploadMultipartThresholdSet multipart part bytes and the byte threshold above which direct uploads become multipart.
directUploadUrlsEndpoints named start, signPart, status, complete, and abort; all are required for standalone direct uploads.
uploadRulesTokenEncrypted package validation profile sent with managed upload requests. Applications normally obtain it from serialized PHP props.
requiresUploadRulesTokenRequire 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:

php
'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:

php
File::make('avatar')
    ->image()
    ->maxSize(2048)
    ->maxDimensions(width: 1000, height: 1000);

Submit-time rules such as required() run when the form is submitted:

php
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:

php
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:

php
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:

php
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:

php
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:

php
$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:

json
{
    "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:

php
// 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:

bash
php artisan form:cleanup-uploads --lifetime=7200

For 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.