solid3d / laravel-tus-s3
Laravel TUS S3 — Tus 1.0 server backed by S3 multipart uploads
v1.0.1
2026-08-11 15:48 UTC
Requires
- php: ^8.5
- illuminate/contracts: ^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- laravel/pint: ^1.14
- orchestra/testbench: ^11.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
README
Laravel Tus 1.0 server backed by S3 multipart uploads. Designed for stateless web pods: no PVC, no shared filesystem, no temporary local upload files.
Requires PHP 8.5+ and Laravel 13.
Protocol subset
| Method | Purpose |
|---|---|
OPTIONS |
Capability discovery |
POST |
Create upload (creation) |
HEAD |
Authoritative Upload-Offset / length / expiry |
PATCH |
Upload next chunk |
DELETE |
Abort (termination) |
Extensions: creation, expiration, checksum, termination.
Not implemented: concatenation, creation-with-upload.
Architecture
HTTP (TusUploadController)
→ TusUploadStore (DurableTusUploadStore)
→ PostgreSQL (tus_uploads: offset, parts, multipart id, expiry)
→ MultipartUploader
→ S3MultipartUploader (production)
→ LocalMultipartUploader (local / Storage::fake)
- Object keys are always generated server-side under
tus.temporary_prefix(defaulttus/tmp/{ulid}). - The Laravel disk
rootis applied by Flysystem /S3KeyResolver— clients cannot choose bucket or key. - PATCH takes a short row lock, uploads the part outside the transaction, then commits ETag/offset atomically.
- If S3 succeeds but the DB update fails, the next PATCH reconciles via
ListParts.
Configuration
| Env | Default | Notes |
|---|---|---|
TUS_STORAGE_DISK |
FILESYSTEM_DISK / s3 |
Disk for temporary objects |
TUS_TEMPORARY_PREFIX |
tus/tmp |
Relative to disk root |
TUS_UPLOAD_EXPIRATION |
60 |
Minutes |
TUS_PATH |
tus |
Route prefix |
TUS_MIN_PART_SIZE |
5242880 |
S3 non-final part minimum (5 MiB) |
TUS_MAX_PART_BYTES |
5242880 |
Bounds checksum buffering; keep Uppy chunkSize ≤ this |
Publish config (optional):
php artisan vendor:publish --tag=tus-config
Uppy
.use(Tus, { endpoint: '/tus', chunkSize: 5_242_880, // >= 5 MiB for S3 multipart })
Operations
php artisan tus:prune # abort expired multipart uploads; delete stale rows
Schedule hourly. Safe to run repeatedly.
Required S3 permissions
s3:CreateMultipartUploads3:UploadParts3:CompleteMultipartUploads3:AbortMultipartUploads3:ListMultipartUploadPartss3:DeleteObjects3:GetObject(finalization streams the temp object)
Scope keys to {disk-root}/tus/tmp/*.
Events
Solid3d\LaravelTusS3\Events\FileUploadCreated($tusFile)Solid3d\LaravelTusS3\Events\FileUploadFinished($tusFile)
TusFile: id, path (relative object key), disk, metadata.
Finalization note
Prefer streaming the completed temporary object (Storage::readStream). If a downstream library requires a local path, spool with a hard byte bound and delete both the spool and the S3 temp object afterward.