Nova Paperclip gives a Nova resource a drag‑and‑drop file attachment field backed by a polymorphic attachments table, so the same field can be dropped onto any number of models without schema changes per model.
composer require daniel-de-wit/nova-paperclip — 2.3.0 requires Laravel 10/11 and a Nova version that supports Laravel 11 (Nova 4.32+). If you are still on Nova 3, stay pinned to 2.2.x.php artisan vendor:publish --tag=paperclip-migrations then php artisan migrate. Nothing renders in Nova until this table exists.config/paperclip.php or similarly tagged) and set the default disk, directory, and any file‑size/type limits. php artisan storage:link if you use the public disk.class Invoice extends Model
{
use HasPaperclip; // trait name per README
}
// app/Nova/Invoice.php
public function fields(Request $request)
{
return [
Paperclip::make('Files')
->disk('s3')
->accept(['application/pdf', 'image/*']),
];
}
First real use case: attach a signed PDF or scanned receipt to an order, view/download it from the Nova detail page, and let support staff replace it without a deploy. Validate it end‑to‑end on staging before rolling to multiple models — the polymorphic relation is the part most likely to surprise you.
One field, many models. Because attachments are polymorphic, the usual pattern is a shared trait plus a small Paperclip::make('Files') field on every resource that needs it. Keep the disk/path configuration in the config file rather than per‑field so a future move from local to S3 is a one‑line change.
Naming the field per use case. When a model has more than one attachment slot (e.g. contract and appendices), use the field/collection name to segregate them rather than relying on MIME filters. This keeps queries in your app code ($invoice->getMedia('contract')-style accessors) readable.
Disk strategy. Use public for anything shown to end users via Nova previews; use a private disk plus signed/temporary URLs for PII, contracts, and ID documents. Set visibility explicitly — the default in a lot of setups is inheriting the disk default, which is not always what you want.
Authorization. Wrap the field in canSee/readonly closures so a support role can download but not replace, and only the owning team can upload:
Paperclip::make('Files')
->canSee(fn ($request) => $request->user()->can('viewFiles', $this->resource))
->readonly(fn ($request) => ! $request->user()->can('uploadFiles', $this->resource)),
Server‑side usage. The trait is the integration point for the rest of the app: generate download URLs in Blade/mail, attach files from a queued job (e.g. a generated report), and copy attachments when duplicating a model. Treat the trait's attach/detach API as the only supported mutation path — writing rows into attachments directly bypasses any path/disk normalization.
Testing. Storage::fake('local') and assert the attachment row plus the file presence; this is much faster than hitting the Nova upload endpoint, and you only need one Nova Dusk/browser test for the drag‑and‑drop UI itself.
Upgrade path. For 2.3.0, run composer update daniel-de-wit/nova-paperclip -W and then composer why-not daniel-de-wit/nova-paperclip 2.3.0 if it refuses — the blocker is almost always a Nova or framework constraint elsewhere, not this package. Re‑run the migration check on a fresh clone to confirm nothing new shipped.
php artisan config:clear and confirm the attachments table exists before debugging anything else.Model::where(...)->delete() or a raw query leaves orphaned files on disk and orphan rows in attachments. Use a scheduled prune command that reconciles the table against storage.upload_max_filesize, post_max_size), and your web server (client_max_body_size in nginx) all need to agree, or large files fail with a generic validation error that looks like a package bug.public disk without php artisan storage:link produce 404s in the Nova preview while the upload itself appears successful.How can I help you explore Laravel packages today?