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:
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
disk | VARCHAR(20) | Storage backend: local or s3 |
path | VARCHAR(500) | Relative file path within the storage disk |
filename | VARCHAR(255) | Original filename |
mime_type | VARCHAR(100) | MIME type (e.g., image/jpeg, application/pdf) |
size | BIGINT | File size in bytes |
width | INTEGER | Image width in pixels (nullable for non-images) |
height | INTEGER | Image height in pixels (nullable for non-images) |
variants | JSONB | Generated image variants (thumbnails, resized versions) |
alt_text | VARCHAR(255) | Alternative text for accessibility |
caption | VARCHAR(500) | Optional caption or description |
metadata | JSONB | Additional file metadata |
model_type | VARCHAR(100) | Parent model type (e.g., content, user) |
model_id | UUID | Parent model primary key |
collection | VARCHAR(100) | Named group (e.g., featured, gallery, avatar) |
created_at | TIMESTAMP | Record creation timestamp |
updated_at | TIMESTAMP | Last 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:
{
"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:
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:
media_service.detach(media_id).await?;get_for_model()
Retrieve all media files for a specific model and collection:
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:
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:
| Provider | Description |
|---|---|
local | Stores files on the local filesystem |
s3 | Stores 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
| Feature | Description |
|---|---|
| Drag-and-Drop Upload | Drop files directly onto the upload zone for instant upload |
| Multiple File Upload | Select or drop multiple files at once |
| Image Thumbnails | Preview images directly in the table |
| File Type Icons | Visual icons for PDFs, documents, videos, and other file types |
| Copy URL | One-click copy of the full file URL to clipboard |
| Open in New Tab | Quick link to view the file in a new browser tab |
| Search & Filter | Filter files by filename, type, or collection |
| Sortable Columns | Sort by filename, size, or upload date |
Permissions
The Media Manager respects the following permissions:
| Permission | Action |
|---|---|
media.view | View the media list and access the Media Manager page |
media.upload | Upload new files (shows/hides the upload zone) |
media.delete | Delete files (shows/hides the delete action) |
Column Definitions
The data table displays the following columns:
| Column | Content |
|---|---|
| Select | Checkbox for bulk selection |
| Preview | Thumbnail for images, file type icon for others |
| Filename | Original filename with MIME type |
| Type | Badge indicating file type (Image, Video, PDF, Document, File) |
| Size | Human-readable file size (KB/MB) |
| Collection | The collection the file belongs to |
| Uploaded | Upload date |
| Actions | Copy 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_typeormodel_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.
# forge.yaml
media:
variants:
thumbnail:
width: 150
height: 150
fit: cover
medium:
width: 600
height: 400
fit: contain
large:
width: 1200
height: 800
fit: containTIP
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.