Media Library
The Media Library uses the selected Supabase Storage, Neon Object Storage, or AWS S3 adapter. It supports images and documents with security validation including file type checking and magic bytes verification.
Drag & Drop Upload
Upload files via drag and drop or file picker with progress tracking.
Security Validation
File type validation, magic bytes verification, and sanitized filenames.
Folder Organization
Organize files in folders: uploads, images, documents, videos.
URL Copy
Copy public URLs with one click to paste into content.
Configuration
The default bucket name is media and can be changed with STORAGE_MEDIA_BUCKET. The provider-selected module in lib/cms/storage.ts handles listing, upload, rename, public URL generation, and cleanup. Supabase init creates or attests the bucket from database/overlays/integrations/supabase-storage/. Neon Object Storage and AWS S3 buckets are provisioned outside the application and expose public reads at STORAGE_MEDIA_PUBLIC_URL; writes remain server-only.
TypeScript Types
Media types are defined in lib/cms/types.ts and include interfaces for uploaded files, storage metadata, and media library items with their public URLs and dimensions.
Storage Functions
The storage module provides functions for uploading files with collision-resistant paths, getting public URLs, bounded directory listing, rename, and deletion. Supabase uses its server service client. Neon Object Storage and AWS S3 use static server credentials through the AWS SDK with abortable deadlines and bounded retries. Provider errors are normalized before reaching routes or users.
Media API
| Endpoint | Method | Description |
|---|---|---|
/api/admin/cms/media |
GET | List files (with pagination) |
/api/admin/cms/media |
POST | Upload file (multipart/form-data) |
/api/admin/cms/media |
PATCH | Rename file |
/api/admin/cms/media?path=xxx |
DELETE | Delete file |
Security Validation
The media upload validates files in multiple ways: a MIME-type allowlist (images + documents only — SVG is intentionally excluded because the format can carry scripts and the bucket is public; use PNG/WebP, or serve trusted SVGs from /public), magic-byte verification, a hard 10 MB per-file size cap, and filename sanitization before the object is written. CMS block content is additionally validated as a JSON object (no script injection) with a 500 KB ceiling, and block keys are restricted to alphanumeric + underscores.
Provider Setup
The media bucket policies (see database/overlays/integrations/supabase-storage/storage-bucket-for-media.sql) are not membership-scoped. Instead they implement a public-read / admin-write model:
- SELECT: public — anyone (including unauthenticated visitors) can fetch any object from the
mediabucket. This is what makes uploaded URLs usable in marketing pages and blog posts. - INSERT / UPDATE / DELETE: restricted to authenticated users whose
profiles.is_administrue. There is no per-account scoping on the bucket itself — media is treated as a platform-wide asset library.
If you need per-account or per-workspace media isolation, you must either add additional bucket policies that join through memberships, or use a separate bucket per account.
For Neon Object Storage or AWS S3, configure the same public-read/server-write boundary with provider IAM and bucket settings. Keep the documents bucket private. Neon rename intentionally uses Get/Put/Delete rather than CopyObject, which is not in Neon's advertised compatibility set; a completed copy followed by an interrupted delete is safely recognized on retry.