netipar/laravel-chunky
Resumable, chunk-based file uploads for Laravel. Send large files reliably over flaky connections with server-side chunk tracking and fingerprint-based resume. Typed backend plus Vue, React, Alpine and Livewire frontends. No queue worker or broadcasting required by default.
Architecture fit. Chunky is architecturally opinionated in ways that suit a Laravel monolith or an Inertia/SPA hybrid rather than a distributed platform. The backend is a ports-and-adapters design (tracker contract with database and filesystem drivers, transport contract with standard and direct_s3), and the frontend is a single framework-agnostic engine with thin Vue 3 / React / Alpine / Livewire adapters. For us that means one upload subsystem to reason about across all our UI surfaces instead of per-framework reinventions. The genuinely valuable architectural property is the module-level manager on the client: uploads are owned outside the component tree, so SPA navigation does not cancel in-flight transfers. That is the single hardest thing to retrofit into an ad-hoc uploader, and it is the reason to consider adopting rather than building.
The second notable fit is the profile abstraction. Validation, disk, directory, authorization, filename, and the post-assembly completed() hook are colocated in one class. That maps cleanly onto our typical per-domain media workflows (avatars, documents, gallery assets) and gives us an auditable policy surface. The Chunky::simple() shortcut covers throwaway cases without a class. Worth noting the batch endpoint validates against the batch's profile, which closes an obvious privilege-escalation hole — a sign the author has thought about abuse, not just the happy path.
Integration feasibility. Low-friction on paper: composer require + php artisan chunky:install + migrate on the backend; a single configure({ baseUrl, headers }) call on the frontend; one service-provider registration per profile. The default auto assembly mode ships without a queue worker or broadcasting, which is the correct default for teams that do not want upload infrastructure as a prerequisite. Config is validated at boot into a typed ChunkyConfig and fails fast on a bad key — good for catching misconfiguration at deploy time rather than at 2 a.m. under load.
The friction points are real, though. The package targets PHP 8.3+ and Laravel 12/13 only. Any application on Laravel 10/11, or on PHP 8.2, is out unless we upgrade first. That is a hard gate and should be the first thing we verify across the estate. The frontend packages live under the @netipar npm scope, so we inherit a single-org supply-chain dependency for four packages; pinning and lockfile review are mandatory.
Technical risk. Ranked:
SECURITY.md, and an OpenAPI spec are all present and the code is PHPStan-max / strict-TS — this reads as a competent, disciplined author, not a weekend script. But documentation quality is not evidence of operational durability. There is no track record of how it behaves under concurrent production load.UPGRADE.md. A maintainer willing to break the world at 1.0 is a maintainer who may break it again at 1.1. We need a stated semver commitment for the profile API, the wire protocol, and the frontend caller API.chunky_* tables under a fingerprint scheme, switching away means migrating or abandoning in-flight uploads. Mitigated by the fact that final artifacts land on our own disks in our own paths — the durable output is portable even if the tracking layer is not.ExposeHeaders: ETag, which is an infrastructure change we must schedule with whoever owns the buckets.upload_expired, invalid_state, checksum_mismatch, lock_timeout) and retry-on-code rather than retry-on-message is exactly the contract we want. lock_timeout in that list tells us concurrency control is in play and can be a source of user-visible retries.Key questions.
ChunkUploaded / chunk-POST idempotent, and how are duplicate final chunks handled?completed() pre- or post-persistence on the final disk?chunky:doctor cover the tracker driver and lock backend for the filesystem driver as thoroughly as the database driver?Stack fit. Ideal if we are on Laravel 12/13 + PHP 8.3 with any of Vue 3, React, Alpine, or Livewire already in the stack — all four are first-class. Blade + Alpine + Livewire shops get the least friction (<livewire:chunky-upload profile="avatar" /> is a drop-in). Teams on a non-supported frontend still have @netipar/chunky-core as a plain TS engine, which is the escape hatch.
Migration path. Brownfield has two shapes. For new upload surfaces, adopt Chunky directly — no migration. For existing upload endpoints, run Chunky alongside the legacy path behind a distinct route prefix (/api/chunky) and a distinct profile per use case, then cut over surface by surface. Do not attempt to migrate in-flight uploads; let the legacy path drain. The published chunky_* tables are additive and reversible, so a rollback is a route/config change plus a table drop, provided we have not yet made Chunky paths the only source of truth for media records.
Compatibility. Three hard checks before any commitment: (1) Laravel and PHP versions across all target apps; (2) whether the aws/aws-sdk-php requirement for direct_s3 conflicts with our existing SDK pin; (3) npm scope approval — whether our registry policy permits adding four @netipar/* packages and how we mirror/pin them. Also confirm the frontend packages' TypeScript/bundler expectations against our build (Vite is the safe assumption for Laravel).
Sequencing. Recommended five-step ramp:
sync mode on a staging app. Exercise pause/resume/reload-resume, the fileChecksum verification path, and forced failure injection (kill the network mid-upload, expire an upload, corrupt a chunk).chunky:doctor into the CI deploy gate with --wait for the queue probe; confirm it exits non-zero on a deliberately broken disk config.auto mode so small files never touch a worker.direct_s3 only after the CORS change is confirmed and the R2 caveat is accepted or avoided, and only then consider batch uploads.Build-vs-buy note. The realistic alternatives are (a) a tus-protocol stack, (b) an older chunk-upload package, or (c) in-house. In-house costs roughly the sum of: a resumable client, a chunk state machine, two tracker drivers, four framework adapters, and a protocol spec — weeks of work to reach parity with what is documented here. That asymmetry is the strongest argument for a spike. The weakest argument for a full commitment is the 0-star, single-maintainer profile.
Maintenance. The package moves independently of our app. Budget a standing line item for version bumps and reading UPGRADE.md on minor releases, given the 0.x → 1.0 rewrite precedent. Our own maintenance surface is the profile classes — small, testable, and the natural place to own business logic. The typed config and boot-time validation mean config mistakes surface at deploy, which converts a class of runtime incidents into failed deploys. That is a good trade.
Support. No commercial support, no vendor. Our support story is: read the docs (they are genuinely good, in two languages, with an OpenAPI spec), read the source (it is small and typed), or fork. That is acceptable for a non-critical path and uncomfortable for a core revenue path. Recommend we do not put this behind a regulated or paid-critical upload flow until we have a quarter of clean production telemetry. Assign a named internal owner so the fork option stays credible.
Scaling. The auto mode with a 256 MB sync threshold is a sensible default but it is a web-worker ceiling: sync assembly holds a PHP request open while streaming a merge. Above the threshold it goes to queue, so we must have a worker running and monitored before we accept large-file use cases at volume. direct_s3 is the real scaling lever — PHP never touches bytes and S3 CompleteMultipartUpload is the assembly, eliminating the merge step entirely. If upload volume is a genuine dimension of the product, plan for direct_s3 as the end state, not sync. Concurrency on the client is a secondary tuning knob worth benchmarking rather than accepting blind.
Failure modes. The documented error envelope gives us a clean operational vocabulary:
upload_expired) — expected under abandonment; must be paired with scheduled chunky:cleanup or the tracker table and chunk files grow without bound. This is the most likely self-inflicted incident: forgetting the cron entry. Put it in the deploy checklist, not just the README.lock_timeout — concurrency contention; user-visible as retryable failure. Watch the rate; a rising rate means the chunk POST path is saturating.checksum_mismatch — silent corruption caught, which is the good outcome, but it needs a client retry policy and a server-side alert.chunky:cleanup is the janitor. Verify it removes both tracker rows and chunk artifacts on the filesystem driver.ETag — the classic direct-to-S3 breakage; surfaces as client-side failures that look like network errors. Test the CORS recipe in staging, not production.queue mode — the worst UX failure: uploads hang at "assembling" with no error. This is precisely what chunky:doctor's worker probe exists to prevent; wire it into the deploy gate and into a periodic health check.disk, final_path, user_id withheld), but we should verify on our own before trusting it in a multi-tenant context.Ramp-up. Backend engineers already fluent in Laravel need hours, not days — the profile class is idiomatic and the quickstart is accurate. Frontend engineers need a mental-model shift on one point: unmounting a component unsubscribes, it does not cancel. Teams conditioned to useEffect cleanup killing requests will get this wrong and will report "uploads vanish on navigation" as a bug. Put it in the onboarding note. Add manager.installUnloadGuard() early so real page unloads warn, while SPA navigation stays silent — that distinction is only obvious once explained. Budget roughly one engineer-week for the spike plus instrumentation, and gate the broader rollout on a fortnight of telemetry showing a chunk-retry rate and lock_timeout rate we are happy with.
How can I help you explore Laravel packages today?