Skip to content

Media Management

FORGE includes a polymorphic file upload system that allows any model in your application to have associated files and images. The system supports local and S3 storage, automatic image variant generation, and a drag-and-drop upload interface in the admin panel.

Overview

The media system follows a Spatie-style polymorphic association pattern. Rather than adding file columns to every table, all uploads are stored in a central media table and linked to their parent model via model_type and model_id columns. A collection field allows multiple distinct file groups per model (for example, a "featured" image and a "gallery" collection).

Database Schema

The media table stores all uploaded files:

ColumnTypeDescription
idUUIDPrimary key
diskVARCHAR(20)Storage backend: local or s3
pathVARCHAR(500)Relative file path within the storage disk
filenameVARCHAR(255)Original filename
mime_typeVARCHAR(100)MIME type (e.g., image/jpeg, application/pdf)
sizeBIGINTFile size in bytes
widthINTEGERImage width in pixels (nullable for non-images)
heightINTEGERImage height in pixels (nullable for non-images)
variantsJSONBGenerated image variants (thumbnails, resized versions)
alt_textVARCHAR(255)Alternative text for accessibility
captionVARCHAR(500)Optional caption or description
metadataJSONBAdditional file metadata
model_typeVARCHAR(100)Parent model type (e.g., content, user)
model_idUUIDParent model primary key
collectionVARCHAR(100)Named group (e.g., featured, gallery, avatar)
created_atTIMESTAMPRecord creation timestamp
updated_atTIMESTAMPLast update timestamp

Polymorphic Association

Files are linked to any model using the combination of model_type, model_id, and collection:

┌──────────────┐       ┌───────────────────────────────────────┐
│   contents   │       │               media                   │
│──────────────│       │───────────────────────────────────────│
│ id: uuid-1   │◄──────│ model_type: "content"                 │
│ title: About │       │ model_id:   "uuid-1"                  │
│              │       │ collection: "featured"                 │
└──────────────┘       │ filename:   "hero.jpg"                 │
                       └───────────────────────────────────────┘

┌──────────────┐       ┌───────────────────────────────────────┐
│    users     │       │               media                   │
│──────────────│       │───────────────────────────────────────│
│ id: uuid-2   │◄──────│ model_type: "user"                    │
│ name: Jane   │       │ model_id:   "uuid-2"                  │
│              │       │ collection: "avatar"                   │
└──────────────┘       │ filename:   "profile.png"              │
                       └───────────────────────────────────────┘

Variants Object Structure

Image variants are generated on upload and stored as JSONB:

json
{
  "thumbnail": {
    "path": "media/content/uuid-1/thumb_hero.jpg",
    "width": 150,
    "height": 150
  },
  "medium": {
    "path": "media/content/uuid-1/med_hero.jpg",
    "width": 600,
    "height": 400
  },
  "large": {
    "path": "media/content/uuid-1/lg_hero.jpg",
    "width": 1200,
    "height": 800
  }
}

MediaService API

The MediaService provides methods for managing file associations:

attach()

Upload and associate a file with a model:

rust
let media = media_service.attach(
    file,          // Uploaded file data
    "content",     // model_type
    content.id,    // model_id
    "featured"     // collection name
).await?;

detach()

Remove a file association and delete the file from storage:

rust
media_service.detach(media_id).await?;

get_for_model()

Retrieve all media files for a specific model and collection:

rust
let images = media_service.get_for_model(
    "content",     // model_type
    content.id,    // model_id
    "gallery"      // collection name
).await?;

get_first_for_model()

Retrieve the first (or only) media file for a model and collection:

rust
let avatar = media_service.get_first_for_model(
    "user",        // model_type
    user.id,       // model_id
    "avatar"       // collection name
).await?;

Storage Providers

The media system supports two storage backends:

ProviderDescription
localStores files on the local filesystem
s3Stores files in Amazon S3 or S3-compatible storage

Storage configuration is managed through the Storage Providers system. See that section for detailed setup instructions.

TIP

You can mix storage providers across collections. For example, store user avatars locally for fast access and large gallery images on S3.

Admin Interface

The admin panel provides a rich media management experience through a dedicated Media Manager page at /media.

Media Manager Page

The Media Manager provides a centralized interface for uploading and managing standalone media files (files not attached to any specific model):

┌─────────────────────────────────────────────────────────┐
│                   Media Manager                          │
├─────────────────────────────────────────────────────────┤
│  ┌───────────────────────────────────────────────────┐  │
│  │         Upload Zone (Drag & Drop)                 │  │
│  │  [Icon] Click to upload or drag files here        │  │
│  │         Supports multiple files                   │  │
│  └───────────────────────────────────────────────────┘  │
│                                                         │
│  ┌───────────────────────────────────────────────────┐  │
│  │                  Data Table                        │  │
│  │  [ ] │ Preview │ Filename │ Type │ Size │ Actions │  │
│  │  [ ] │ [img]   │ logo.png │ Image│ 156KB│ [...]   │  │
│  │  [ ] │ [icon]  │ doc.pdf  │ PDF  │ 2.4MB│ [...]   │  │
│  │  [ ] │ [icon]  │ data.csv │ File │ 89KB │ [...]   │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘

Features

FeatureDescription
Drag-and-Drop UploadDrop files directly onto the upload zone for instant upload
Multiple File UploadSelect or drop multiple files at once
Image ThumbnailsPreview images directly in the table
File Type IconsVisual icons for PDFs, documents, videos, and other file types
Copy URLOne-click copy of the full file URL to clipboard
Open in New TabQuick link to view the file in a new browser tab
Search & FilterFilter files by filename, type, or collection
Sortable ColumnsSort by filename, size, or upload date

Permissions

The Media Manager respects the following permissions:

PermissionAction
media.viewView the media list and access the Media Manager page
media.uploadUpload new files (shows/hides the upload zone)
media.deleteDelete files (shows/hides the delete action)

Column Definitions

The data table displays the following columns:

ColumnContent
SelectCheckbox for bulk selection
PreviewThumbnail for images, file type icon for others
FilenameOriginal filename with MIME type
TypeBadge indicating file type (Image, Video, PDF, Document, File)
SizeHuman-readable file size (KB/MB)
CollectionThe collection the file belongs to
UploadedUpload date
ActionsCopy URL, Open in new tab, Delete

Standalone vs. Attached Media

The Media Manager displays all media files regardless of whether they are attached to a model:

  • Standalone media: Files uploaded directly through the Media Manager (no model_type or model_id)
  • Attached media: Files associated with a model (e.g., user avatars, content images)

Both types appear in the Media Manager for centralized management.

WARNING

Deleting a parent model record does not automatically delete associated media files. Make sure your delete handlers call media_service.detach() for all associated media to avoid orphaned files in storage.

Image Variant Generation

When an image file is uploaded, the system automatically generates configured variants (thumbnail, medium, large). Variant dimensions and quality settings are determined by the application configuration.

yaml
# forge.yaml
media:
  variants:
    thumbnail:
      width: 150
      height: 150
      fit: cover
    medium:
      width: 600
      height: 400
      fit: contain
    large:
      width: 1200
      height: 800
      fit: contain

TIP

Variant generation only applies to image files (JPEG, PNG, WebP, GIF). Non-image uploads such as PDFs or documents are stored as-is without variant processing.

Released under the MIT License.