Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Filepond Laravel Package

digital-creative/nova-filepond

Nova Filepond is a Laravel Nova field that integrates the FilePond upload library, giving you drag-and-drop, image previews, and smooth asynchronous file uploads inside your Nova admin panels with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register. The package is a Nova field, so it only makes sense inside an existing Nova app.

composer require digital-creative/nova-filepond
php artisan vendor:publish --tag=nova-filepond-config   # optional but recommended

Assets are auto-registered by Nova — there is no npm/mix step and nothing to add to webpack.mix.js. If you don't see FilePond's drag-and-drop UI after a page load, the problem is almost never assets; it's a stale Nova cache. php artisan nova:publish / hard refresh in the browser clears most of it.

Look at these three things first, in this order:

  1. config/nova-filepond.php — default disk, temporary file path, route names, route middleware. This is the sanctioned override surface.
  2. config/filesystems.php — because if you don't call ->disk() explicitly, the field inherits filesystems.default, which is local on a stock Laravel install. Files land in storage/app and the browser cannot reach them. This is the single most common "it uploaded but the image is broken" cause.
  3. Your model — because the field writes relative paths, not URLs, into the column.

First use case: a single avatar on a public disk.

use DigitalCreative\Filepond\Filepond;

public function fields(Request $request): array
{
    return [
        Filepond::make('Avatar')
            ->disk('public')
            ->prunable()
            ->rules('nullable', 'image', 'max:2048')
            ->acceptedFileTypes(['image/jpeg', 'image/png', 'image/webp']),
    ];
}
php artisan storage:link

Two things to do at the same time as that first field, not later:

  • Resolve the URL somewhere explicit. The column holds avatars/abc123.jpg. Don't sprinkle Storage::url($model->avatar) through Blade and Nova; add an accessor.
protected function avatarUrl(): Attribute
{
    return Attribute::get(
        fn () => $this->avatar ? Storage::disk('public')->url($this->avatar) : null
    );
}
  • Duplicate the validation server-side. FilePond's validation is client-side and advisory; the upload endpoint can be hit directly. The ->rules() above is the one that actually protects you.

Implementation Patterns

Single file: string column

The default. Column type string, nullable for optional uploads. On save the field writes the relative path; on the edit screen it hydrates from the Load Controller so the existing image previews with the file name.

Multiple files: array column with a cast

->multiple() writes a JSON array of relative paths. Cast it, or you'll be json_decode-ing in every view.

Filepond::make('Gallery')
    ->disk('public')
    ->multiple()
    ->prunable()
    ->rules('array')
    ->acceptedFileTypes(['image/*']);
protected $casts = ['gallery' => 'array'];

FilePond preserves the order in which files were added, so the array order is the gallery order — no separate order column needed for a simple case.

S3 / private disks

For a non-public disk, don't try to make Storage::url() work. Use temporary URLs and let Nova/the front-end treat them as short-lived reads:

Filepond::make('Contract')->disk('s3')->prunable();
protected function contractUrl(): Attribute
{
    return Attribute::get(
        fn () => $this->contract
            ? Storage::disk('s3')->temporaryUrl($this->contract, now()->addMinutes(30))
            : null
    );
}

If previews 401 or expire on the edit screen, this is the layer to look at — the Load Controller is where the read URL is produced.

Pair it with a display field

For read-only surfaces (index views, detail pages), use a Nova Image/Url field fed by the accessor and keep Filepond for the form:

Image::make('Avatar', 'avatar_url')->thumbnail(fn () => $this->avatarUrl),
Filepond::make('Avatar')->disk('public')->onlyOnForms()->prunable(),

Passing raw FilePond options

Anything the field doesn't expose as a fluent method (preview height, image crop, chunked uploads, custom labels) goes through the options array:

Filepond::make('Video')
    ->disk('s3')
    ->multiple()
    ->options([
        'imagePreviewHeight' => 170,
        'allowImageCrop'     => true,
        'chunkUploads'       => true,
        'chunkSize'          => 5_000_000,
    ]);

Treat this as an escape hatch: options here are passed to the JS library, so they are not reflected in your server-side rules.

Custom load/store behaviour

There are no events to hook. If you need custom path handling, tenancy-scoped paths, or a bespoke authorization check, subclass the field and override load()/store() rather than editing vendor controllers.

class TenantFilepond extends Filepond
{
    public function store(Request $request): array
    {
        // scope to the current tenant, then delegate
        return parent::store($request);
    }
}

Pruning temporary uploads

Uploads land in a temp directory before the record is saved, so orphaned files accumulate. Schedule the prune command the package/config provides in every environment, including staging, and confirm it actually runs:

// routes/console.php or Kernel::schedule
$schedule->command('model:prune')->daily();

->prunable() on the field covers deletion with the model — see the caveat below for soft deletes.

Testing

Fake the disk and assert on the stored path, not the URL:

Storage::fake('public');

$response = $this->actingAs($admin)->post(...);

Storage::disk('public')->assertExists($model->fresh()->avatar);
$this->assertStringStartsWith('avatars/', $model->fresh()->avatar);

Include at least one test for the custom-path flow if you use one — that's the code path v1.0.6 changed.


Gotchas and Tips

Omitting ->disk() is a bug waiting to ship. The field inherits filesystems.default. On local that means files are outside the web root. Be explicit on every field, every time, even when you think the default is already public.

The column holds a path, not a URL. If you see /storage/avatars/x.jpg in your database, something in your store path is doing more than it should. Keep URL resolution in one accessor or one display field.

v1.0.6 changes filename display — audit your shims. Prior to 1.0.6, a custom path in the Load Controller caused previews to render avatars/photo.jpg instead of photo.jpg, and people worked around it with ->displayUsing(fn ($v) => basename($v)) or a load controller that stripped directories. v1.0.6 fixes it at the source. If you upgrade, deliberately remove those shims and re-test both default and custom-path flows — leaving them in place double-strips names.

prunable() fires on model delete, not soft delete. On a soft-delete-heavy schema, the files stay. Attach an observer on forceDeleted, or prune on the deleted event yourself.

Client-side validation is advisory. maxFileSize maps to FilePond's option, which is in bytes — 2 * 1024 * 1024 for 2 MB — while Laravel's max: rule on files is in kilobytes — max:2048. Getting these out of sync is easy and produces a UI that accepts a file the server then rejects with a 422 the user can't see.

Broken previews are the #1 support ticket, and it's almost always 401/403. Two causes: route middleware in the published config not matching the guard Nova authenticates against, or an expired/unsigned URL on a private disk. Open the network tab, find the load request, and check the status before touching anything else. php artisan route:list | grep -i filepond tells you which middleware is actually applied.

No events, no hooks. There is no FilepondUploaded event to listen for. Anything you need to do on upload is a subclass override or a model observer on save. Plan for that permanently-in-house subclass early if you know you'll need it.

Check the support matrix before you commit. Last release 2024-01-31, small maintainer base. A Nova major-version mismatch doesn't fail at composer require — it fails as a silently dead front-end component. Pin the version in composer.json, and verify against your Nova/Laravel/PHP versions in a staging branch first.

Chunked uploads are not free. Turning on chunkUploads for large video is easy client-side and then has to clear your web server's body-size limits, proxy timeouts, and the storage backend's multipart behaviour. Exercise it end-to-end against the real infrastructure, not just locally.

Debugging checklist, in order: Is the field's ->disk() set and does storage:link exist? Does the load request return 200? Is the route middleware correct? Does the DB column contain a bare relative path? Is maxFileSize in the right unit? Ninety percent of issues resolve in those five questions.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect