vadage/presigned-uploader-bundle
Symfony bundle that generates presigned upload URLs so clients can send files directly to cloud storage such as S3. Keeps credentials server-side, offloads large transfers from your application, and simplifies secure direct-to-storage uploads with minimal configuration.
composer require vadage/presigned-uploader-bundle; the browser side is npm i @vadage/presigned-uploader-bundle. In a Laravel app you will typically use the package for its presigned-URL contract + Stimulus controller, and do the actual signing with Laravel's own Storage facade rather than adopting the bundle's Symfony DI wiring.package.json and dist/upload_controller.js in the published npm tarball. Release v0.2.1 exists specifically because those two files were missing from the export map, so version >=0.2.1 is the first one where the package is actually consumable by an asset build. Then read assets/controllers.json to see the controller's default identifier (upload) and its fetch mode.data-controller="upload" to a file input, point it at a tiny Laravel endpoint that returns Storage::disk('s3')->temporaryUploadUrl($key, now()->addMinutes(10)), and let the controller PUT straight to the bucket — no file body ever transits PHP.Manual controller registration under Laravel. assets/controllers.json is a Symfony UX convention; laravel-vite-plugin does not read it. Register explicitly in resources/js/app.js:
import { Application } from '@hotwired/stimulus'
import UploadController from '@vadage/presigned-uploader-bundle/dist/upload_controller.js'
const app = Application.start()
app.register('upload', UploadController)
Import from dist/... rather than the bare package specifier — that path is now a guaranteed export as of v0.2.1.
Keep the presign endpoint dumb and scoped. A single POST /uploads/presign that validates content_type and size, derives the key server-side (uploads/{user_id}/{uuid}), and returns { url, key, headers }. Never accept a client-supplied key — that is a bucket-write primitive.
Mirror the signature in validation. Whatever headers you sign (usually Content-Type, sometimes Content-Length) must be sent byte-identical by the controller. If the controller sends Content-Type: image/png and you signed image/PNG, S3 returns a 403 that looks like a credential problem.
Drive the UI off Stimulus events, not polling. Bind data-action="upload:progress->form#update" and upload:success->form#enableSubmit; disable the submit button until success so you never persist a row pointing at an object that never landed.
Test the build, not just the PHP. The v0.2.1 bug was invisible to Composer and to any PHP test — it only surfaced at npm run build. Add a CI step that runs the frontend build, which is the only reliable regression guard for this class of packaging failure.
Verify server-side before persisting. After the client reports success, Storage::disk('s3')->exists($key) (or a HEAD) in the form-request handler before writing the DB record.
>=0.2.1. The symptoms are exact and worth recognising: with Webpack Encore the build failed as soon as the package was listed (The file "@vadage/presigned-uploader-bundle/package.json" could not be found); with vite-plugin-symfony it failed only once the upload controller was enabled, because assets/controllers.json eager-resolves the controller module lazily. If you are on v0.2.0 or earlier under Vite, the error appears at controller-enable time, not install time, which makes it easy to misattribute to your own config.node -e "console.log(require.resolve('@vadage/presigned-uploader-bundle/package.json'))" tells you in one line whether the file is actually in the tarball. npm pack --dry-run shows the published file list and catches missing files/exports entries before you blame your bundler.php artisan vendor:publish to give you anything. Everything that matters lives in dist/ and assets/controllers.json.AllowedMethods: PUT, POST, GET, HEAD and ExposeHeaders: ETag on the bucket, and confirm the Origin matches your local port exactly.assets/controllers.json entry tells you exactly which data attributes the controller reads, which is the fastest way to learn the contract without reading dist/ minified source.How can I help you explore Laravel packages today?