YYayaw

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_folders
  • media_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_PROVIDER selects Supabase when Supabase storage env is present
  • empty STORAGE_PROVIDER selects S3 when the S3-compatible env set is complete

Supabase storage requires:

  • NEXT_PUBLIC_SUPABASE_URL
  • SUPABASE_SERVICE_ROLE_KEY

S3-compatible storage requires:

  • STORAGE_MEDIA_BUCKET
  • STORAGE_PUBLIC_BASE_URL
  • S3_ENDPOINT
  • S3_REGION
  • S3_ACCESS_KEY_ID
  • S3_SECRET_ACCESS_KEY
  • S3_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:

  • image
  • video
  • audio
  • pdf
  • document
  • archive
  • other

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:

PlanMax file sizeMax organization storage
Free25 MB1 GB
Pro100 MB10 GB
Business250 MB50 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 media bucket 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_KEY
  • OPENAI_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.tsx
  • src/lib/server/actions/media/media-library-actions.ts
  • src/lib/server/actions/media/media-library-permissions.ts
  • src/lib/server/services/media/media-asset-storage.ts
  • src/lib/server/services/media/media-asset-thumbnails.ts
  • src/lib/server/services/media/media-image-asset-generation.ts
  • src/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