sopamo/laravel-filepond
An all-in-one Laravel backend for FilePond. Handles temporary uploads, chunked uploads, and file reverting, with support for any Laravel filesystem disk. Get up and running in minutes with a simple composer install and minimal JS config.
Check version requirements first
v2.0.0-beta.5 is a breaking release: PHP 8.2+ and Laravel 12 or 13 only. Laravel 13 requires PHP 8.3+.composer.json casually — it will fail resolution or, worse, fail at runtime.php -v && php artisan --version
composer require sopamo/laravel-filepond:2.0.0-beta.5
Pin the exact beta tag. Semver pre-releases don't get patch updates automatically; you'll want deliberate bumps while evaluating.
Publish the config
php artisan vendor:publish --provider="Sopamo\Filepond\FilepondServiceProvider" --tag="filepond-config"
Review disk, path, allowed_mime_types, max_file_size, and the chunk settings. In v2 the chunk pipeline changed, so diff your published config against the upstream default rather than assuming your 1.x values still mean the same thing.
Register the routes
use Sopamo\Filepond\FilepondController;
Route::post('/filepond-upload', [FilepondController::class, 'upload']);
Route::post('/filepond-revert', [FilepondController::class, 'revert']);
Route::get('/filepond-load', [FilepondController::class, 'load']);
FilePond's JS calls revert and load as part of its normal lifecycle (undo, restoring a previously uploaded file). Missing them shows up as silent frontend failures, not backend errors.
Frontend wiring
<link href="https://unpkg.com/filepond@^4/dist/filepond.css" rel="stylesheet">
<script src="https://unpkg.com/filepond@^4/dist/filepond.js"></script>
FilePond.setOptions({
server: {
process: '/filepond-upload',
revert: '/filepond-revert',
load: '/filepond-load',
},
});
First use case to validate the upgrade Upload a small file, then a chunked file, then revert it. If all three work, the temp-upload path handling and cleanup are behaving. Then repeat with your real disk (S3 or Azure) before touching production config.
Basic upload
The stock FilepondController handles validation, disk writes, and FilePond's expected response format. For a straightforward "user uploads an avatar" case, route straight to it and only customize the config.
Custom validation Extend the controller rather than duplicating it:
// app/Http/Controllers/CustomFilepondController.php
use Sopamo\Filepond\FilepondController;
class CustomFilepondController extends FilepondController
{
public function upload(Request $request)
{
$this->validate($request, [
'file' => ['required', 'mimes:jpeg,png,pdf', 'max:10240'],
'user_id' => 'required|exists:users,id',
]);
return parent::upload($request);
}
}
Keep the parent call last so package-level response formatting stays intact.
Chunked uploads
// config/filepond.php
'chunk_size' => '5MB',
'chunk_upload_url' => '/filepond-chunk-upload',
v2 rewrote chunk handling. Invalid chunk ranges and oversized integer headers are now rejected before anything is written to storage — which is good, but it means a client sending odd headers now gets an error instead of a silently-created empty chunk. In practice:
Azure OSS storage Azure chunked uploads now stage and commit blocks server-side instead of downloading the assembled file and re-uploading it. This is the single biggest performance change in the release for large files.
Content-Type when initializing a chunked upload. See the README example. Omit it and blocks are staged with a generic type, which typically bites you later on download or when the file is served from a CDN.FilePond metadata or a hidden form field) rather than inferring it server-side — the browser knows better than a filename extension check.Post-upload processing
// app/Providers/AppServiceProvider.php
use Sopamo\Filepond\Events\FileUploaded;
public function boot(): void
{
Event::listen(FileUploaded::class, function ($event) {
// thumbnails, virus scan dispatch, DB record creation
});
}
Remember this fires per completed upload, which for chunked uploads means after the final commit, not per chunk.
// resources/js/app.js
import * as FilePond from 'filepond';
import 'filepond/dist/filepond.min.css';
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
FilePond.registerPlugin(FilePondPluginImagePreview);
public function rules(): array
{
return ['file' => ['required', 'mimes:jpeg,png', 'max:5120']];
}
Note the package's own config-level validation still applies; keep the two consistent or you'll reject files the config would have allowed (confusing for users, invisible in logs).league/flysystem — set 'disk' => 's3' and confirm your IAM policy allows PutObject and DeleteObject, since revert needs to delete.2.0.0-beta.5, run the full test suite, and exercise upload/chunk/revert manually on each disk you use. The release adds regression tests and CI across supported PHP and Laravel versions — mirror their matrix locally so you're not the one discovering an 8.2-only issue.PHP/Laravel version floor The hardest failure mode is a lockfile that resolves but breaks at boot. Check CI runners too — a pipeline on PHP 8.1 will fail on install, not on a test, which can look like a flaky build if you don't read the log.
Laravel 13 needs PHP 8.3+ Laravel 12 + PHP 8.2 is fine; Laravel 13 + PHP 8.2 is not. If you run a mixed matrix, pin per-job versions explicitly.
Azure Content-Type omission
The most likely regression for existing Azure users. Symptom: uploads succeed but served files have the wrong MIME type, or downstream processing (image previews, PDF viewers) misbehaves. There's no exception thrown — validate it with a test.
Disk-root temp paths
v2 explicitly supports '' and '/' as temp upload paths, along with historical server IDs. If you have legacy temp paths or stored server IDs from 1.x, they should keep working thanks to the added path validation and safe deletion — but verify any custom code that concatenates paths, since '/' plus a filename can produce //name and bypass naive checks.
CORS for cross-origin frontends
// config/cors.php
'paths' => ['filepond-*', 'api/*'],
'allowed_origins' => ['https://your-frontend.com'],
FilePond's load endpoint is a GET — make sure it's not accidentally excluded from your CORS paths.
Filename collisions Defaults use UUIDs. If you override:
'filename_generator' => fn ($file) => 'custom_' . time() . '.' . $file->getClientOriginalExtension(),
time() alone collides under concurrent uploads. Use Str::uuid().
Timeouts on large files
Raise max_execution_time, memory_limit, upload_max_filesize, post_max_size, plus client_max_body_size in Nginx. A 413 from the proxy never reaches Laravel, so nothing appears in your logs.
public function upload(Request $request)
{
try {
return parent::upload($request);
} catch (\Throwable $e) {
\Log::error('FilePond upload failed', ['message' => $e->getMessage(), 'headers' => $request->headers->all()]);
return response()->json(['error' => 'Upload failed'], 500);
}
}
Logging headers matters specifically for the new chunk-header validation — you want the rejected values.Custom storage logic
protected function storeFile($request, $file)
{
$path = $this->getFilePath($file);
Storage::disk($this->config['disk'])->put($path, file_get_contents($file));
return $path;
}
Don't override this for Azure chunked uploads — the block staging/commit path is the point of the v2 change, and bypassing it reintroduces the download-and-reupload behavior.
Pre-signed URLs for direct-to-cloud uploads
$url = Storage::disk('s3')->temporaryUploadUrl(
'uploads/' . Str::uuid(),
now()->addMinutes(15),
['ContentType' => $request->input('content_type')]
);
return response()->json(['url' => $url]);
Prefer Storage::temporaryUploadUrl over instantiating an S3Client yourself — it respects your disk config and credentials.
Post-upload events
event(new \App\Events\FileProcessed($filePath, $request->user()));
Keep custom events distinct from the package's FileUploaded so listeners don't fire on internal temp operations.
Listener registration
Register in EventServiceProvider::$listen (or via Event::listen in boot()), and confirm it's firing with php artisan event:list before assuming your processing logic is broken.
How can I help you explore Laravel packages today?