Media Library
Organization-scoped media assets, folders, quotas, thumbnails, and AI image generation.
Overview
The media library is the shared organization asset store for content workflows. It is available from:
/dashboard/content/media
Media assets are organization-scoped. They are used by the page editor, Page AI, and content workflows that need durable URLs for images, video, audio, PDFs, or other supported files.
CMS Role
Media is the CMS asset layer. It owns binary files, generated thumbnails, generated images, public URLs, quota enforcement, and folder organization. Structured CMS data can reference media URLs, and pages or sections can bind media fields to selected assets, but the asset itself stays in the media library.
Use media when a workflow needs a stored file. Use CMS data for typed content, sections for reusable layout, and page documents for composition.
Data Model
Media uses two system tables:
media_foldersmedia_assets
Assets store:
- owning
organization_id - optional
folder_id - display name and normalized display name
- MIME type and asset kind
- size in bytes
- object-storage bucket and storage key
- public URL
- optional thumbnail metadata
- upload or generation metadata
Folder ownership is scoped to the same organization as the asset. Server actions must reject cross-organization folder or asset mutations.
Storage
Media storage uses a small provider contract. The default hosted path remains Supabase storage, and self-hosted deployments can use an S3-compatible provider such as MinIO, AWS S3, or R2. Storage keys follow the organization path convention:
organizations/<organizationId>/media/<asset>
organizations/<organizationId>/media/thumbnails/<assetId>.webp
global/site-variables/<asset>.<content-hash>.<ext>The database stores publicUrl because published pages and CMS-rendered
content need stable public asset URLs. Existing media URLs are not rewritten
when STORAGE_PROVIDER changes, so operators must keep old public URLs
reachable or run a deliberate migration later.
Provider selection:
STORAGE_PROVIDER=supabase|s3- empty
STORAGE_PROVIDERselects Supabase when Supabase storage env is present - empty
STORAGE_PROVIDERselects S3 when the S3-compatible env set is complete
Supabase storage requires:
NEXT_PUBLIC_SUPABASE_URLSUPABASE_SERVICE_ROLE_KEY
S3-compatible storage requires:
STORAGE_MEDIA_BUCKETSTORAGE_PUBLIC_BASE_URLS3_ENDPOINTS3_REGIONS3_ACCESS_KEY_IDS3_SECRET_ACCESS_KEYS3_FORCE_PATH_STYLE
The media library writes to the configured media bucket, and organization logo
uploads use the organization-logos bucket. When the public base URL is the app
or CDN origin, both bucket prefixes must be routed to the S3-compatible object
store before the app fallback.
The seed script uploads the default global site-variable assets, such as the
logo and social image, into the configured media bucket before publishing the
site-variables/main global-data entry. Existing custom site-variable asset
URLs are preserved; only old seed defaults or previous seed-owned storage URLs
are repaired.
See Deployment Environment Setup for provider setup steps.
Supported Asset Kinds
Asset kind is derived from MIME type:
imagevideoaudiopdfdocumentarchiveother
Visual assets can receive WebP thumbnails. Missing thumbnails are backfilled opportunistically during media listing when the asset is eligible and the retry cooldown allows another attempt.
Permissions
Media operations are organization-scoped.
Typical access:
- organization members can list/read assets
- organization managers/admins/owners can upload, edit, move, and delete assets
- superadmins can operate globally when the underlying authorization allows it
All server actions must include the active organization scope. Navigation or UI visibility is not a substitute for server-side authorization.
Quotas
Upload quotas come from runtime billing plan limits. Defaults are defined in the billing config and can be overridden through billing plan data:
| Plan | Max file size | Max organization storage |
|---|---|---|
| Free | 25 MB | 1 GB |
| Pro | 100 MB | 10 GB |
| Business | 250 MB | 50 GB |
Quota checks happen before persistence. Generated image assets use the same quota path as manual uploads, so AI generation cannot bypass plan limits.
Dashboard UX
The media dashboard supports:
- folder navigation
- upload
- rename and metadata edits
- delete
- kind-aware previews
- deterministic sorting
- thumbnail display for visual assets
- generated image insertion from supported content workflows
Keep the interface dense and operational. Media is a working library, not a marketing gallery.
Page Builder Integration
The page editor can bind media fields to existing organization media assets.
Bindings keep the original asset publicUrl so published pages resolve the
same file that was selected in the picker.
Page AI can generate at most one image asset for a prompt that asks for media or
strongly implies a rich landing-page visual. The generated image is persisted
through the media library pipeline and then bound to a compatible generated
section field such as heroImage.
Global public pages can use generated assets from the active organization's media library because the stored URL is public. If no active organization or media permission is available, image generation is skipped with a warning and the page draft still succeeds.
Thumbnail Pipeline
Thumbnail generation:
- downloads the source visual asset
- converts to WebP
- uploads to the
mediabucket under the thumbnails path - stores thumbnail URL/storage metadata on
media_assets - records failure metadata when conversion or upload fails
Thumbnail failures should not block listing or rendering the original asset. The retry cooldown prevents repeated expensive attempts for the same broken source.
AI Image Generation
Image generation is gated by:
OPENAI_API_KEYOPENAI_IMAGE_GENERATION_ENABLED- the managed site setting
media-image-generation-enabled - media permissions and quotas
Generated images use the configured image model from OPENAI_IMAGE_MODEL and
are saved as WebP media assets.
Server Entry Points
Key files:
src/blocks/dashboard/content/media/index.tsxsrc/lib/server/actions/media/media-library-actions.tssrc/lib/server/actions/media/media-library-permissions.tssrc/lib/server/services/media/media-asset-storage.tssrc/lib/server/services/media/media-asset-thumbnails.tssrc/lib/server/services/media/media-image-asset-generation.tssrc/lib/server/services/media/media-quotas.ts
Operational Notes
- Do not store media binaries in Postgres.
- Do not use a private URL for published page assets unless the runtime also implements signed URL refresh.
- Do not count generated thumbnails against user upload quota.
- Do not allow one organization to select, mutate, or delete another organization's folder or asset.
- Keep Supabase service-role keys and S3 credentials server-only.
Validation
Useful checks after media changes:
bun test src/lib/server/services/media/media-mappers.test.ts
bun test src/lib/server/services/media/media-quotas.test.ts
bun test src/lib/server/services/media/media-asset-storage.test.ts
bun test src/lib/server/services/media/media-asset-thumbnails.test.ts
bun run check
bunx tsc --noEmit