Choose the simplest approach
An API response being JSON does not mean you must store uploads in a JSON database column. This example reads relational rows and returns URLs to the caller.
Do I need to modify FLEX? No. These are application models, routes, presenters and controllers using the existing storage and conversion interfaces. The advanced native index reads FLEX upload receipts, so keep that bridge covered during package upgrades. Do not expose its repository or conversion journals as public endpoints.
Display an original without indexing conversions
For the recipe’s public disk, an already authorized post can supply its original images directly:posts/7/main/photo-deck-<uuid>.jpg is read from the configured disk. You do not need to mount a Filament page or initialize File Upload on the website.
Optional: make named conversions queryable in SQL
Skip this section if you use Spatie or only need originals. A conversion index is useful when your own application needs queries such as “give me thethumb for each gallery image”, dimensions in an API, or eager-loaded image versions for a website grid. It is one possible integration, not a requirement for using images outside a form.
The tables then have a simple relationship:
media records and conversion metadata instead.
Add the conversion table and indexer
database/migrations/2026_10_17_000002_create_gallery_conversions.php
app/Models/GalleryConversion.php
Gallery class:
app/Support/SyncGalleryConversions.php
$row = ...updateOrCreate(...) statement in save():
false until the worker finishes. A server-side reconciliation command covers both delayed processing and retries without depending on delivery of a browser event or a best-effort storage notification:
app/Console/Commands/IndexGalleryConversions.php
routes/console.php
conversions_indexed for the affected rows and rerun the command.
The next examples assume this optional index has been installed and the Gallery::conversions() relationship added. They work while indexing is pending: missing thumbnails/viewer versions fall back to the original.
Share one image representation between Blade and JSON
This small application class reads saved rows and produces display URLs. It performs no upload or conversion work. Controllers authorize the post first and eager-load conversions, so listing images does not run a query for every thumbnail. Disk names and storage paths remain on the server; consumers receive URLs.app/Support/GalleryImageData.php
url(). Never fall back to a public URL when signing fails, and never store an expiring URL as the database path.
Add a website gallery and JSON endpoint
Use a Laravel API Resource to expose only the chosen image properties. See Laravel API Resources.app/Http/Resources/GalleryImageResource.php
update with your own view policy when other authorized users should have read access.
app/Http/Controllers/PostGalleryController.php
routes/web.php
resources/views/posts/gallery.blade.php
data, links and meta response. Each image contains original_url, thumbnail_url, viewer_url and the named conversion metadata. The browser receives no raw upload state. Until a conversion is indexed, the corresponding display URL points to the original; rerunning the indexer updates later reads without changing the original gallery row.
Spatie: use the existing media APIs
With Spatie you normally do not needgallery_conversions, the native indexer or its scheduler. Spatie saves each original as a media row related to your model. It tracks generated conversion names/status on that media record and resolves the actual conversion paths through its own APIs. It does not create a separate SQL row for every converted file.
Use getUrl('thumb') for a conversion and hasGeneratedConversion('thumb') before choosing it. See Spatie’s converted image documentation. This alternative assumes the public main collection and thumb/large conversions from the Spatie recipe.
An authorized JSON endpoint can use the existing media records directly:
app/Http/Controllers/SpatiePostGalleryController.php
routes/web.php
media() query is paginated, so an existing large collection is not returned in one response. Native Spatie ordering, queued conversions and filename/path generators remain authoritative. For private media use authorized temporary URLs, not this public getUrl() example.
The same URLs can be used in your own Blade/JavaScript gallery. A separate conversion table is only useful if your application has an additional reporting or indexing requirement; it is not needed for normal Spatie website/API use.
Verify your integration
- Save two images and reload the edit page. Confirm the original gallery rows still belong to the same post.
- Open the website and JSON routes as that owner. Confirm order, thumbnail URLs and larger viewer URLs.
- With queued conversions, check the original fallback before processing and the converted URLs after processing/indexing.
- Open either route as another user. The example policy must deny access.
- Keep the storage paths in SQL. Do not save the public/temporary URLs returned by the presenter back into your models.