Skip to main content
You can use an uploaded image anywhere in your application. It does not have to be rendered by a FLEX field. Start with A complete Post gallery. This page continues that example and adds a website gallery and a JSON endpoint. The original files are already saved; this page does not upload or convert them again.

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:
For example, a saved original 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 the thumb 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:
This index copies actual server-owned receipts. It does not generate new conversions, replace FLEX’s private receipts, or accept derivative paths from the browser. A conversion filename cannot reliably be inferred from the original basename. The following bridge is specific to native disk uploads and the public storage policy in the original recipe. Spatie uses its own 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
Add this relationship to the existing Gallery class:
app/Support/SyncGalleryConversions.php
For synchronous conversions, optionally call the service immediately after the $row = ...updateOrCreate(...) statement in save():
For queued conversions the same call returns 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
Run Laravel’s scheduler in production. Pending/failed conversions remain unindexed; the indexer does not retry conversion encoding itself. Repair/retry the conversion operation through your server-side recovery workflow. To rebuild an index after an intentional source/conversion migration, reset 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
These URLs are for the recipe’s public files: anyone with a URL can access its bytes. A login-protected JSON endpoint does not make a public disk private. For restricted attachments use private disks and authorized, expiring delivery URLs instead of url(). Never fall back to a public URL when signing fails, and never store an expiring URL as the database path. Use a Laravel API Resource to expose only the chosen image properties. See Laravel API Resources.
app/Http/Resources/GalleryImageResource.php
The controller uses the original recipe’s owner policy for both routes. It paginates images and eager-loads their conversion rows. This example allows the owner to view the gallery; replace update with your own view policy when other authorized users should have read access.
app/Http/Controllers/PostGalleryController.php
routes/web.php
The JSON route uses your existing web session. For a token-based API, use your application’s configured API authentication middleware and keep the same policy check. FLEX does not install or configure an API authentication package.
resources/views/posts/gallery.blade.php
This intentionally minimal Blade view uses the filename as a fallback description. Use the editorial alt text you store for real content images. Add your website’s layout and styling; this is not a Filament or FLEX UI renderer. Generate both URLs using Laravel’s route helpers:
Opening the JSON URL while logged in returns Laravel’s paginated 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 need gallery_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
The owner-scoped 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

  1. Save two images and reload the edit page. Confirm the original gallery rows still belong to the same post.
  2. Open the website and JSON routes as that owner. Confirm order, thumbnail URLs and larger viewer URLs.
  3. With queued conversions, check the original fallback before processing and the converted URLs after processing/indexing.
  4. Open either route as another user. The example policy must deny access.
  5. Keep the storage paths in SQL. Do not save the public/temporary URLs returned by the presenter back into your models.
Last modified on October 17, 2026