-
Notifications
You must be signed in to change notification settings - Fork 20
Description
Overview
Add a Media Library plugin that provides a centralized, self-hosted asset management experience — images, files, and videos in one place. The UI target is a Filestack-style picker: folder tree on the left, grid/list view on the right, drag-to-upload, search, filter by type.
Today every plugin that needs image upload (Blog, CMS, UI Builder) requires consumers to wire up a custom uploadImage override. This plugin replaces that scattered pattern with a first-class, shared asset store.
Core Features
Asset Management
- Upload files via drag-and-drop or file picker (images, PDFs, videos, any file)
- Folder tree — create, rename, delete folders
- Grid view (thumbnails) and list view (name, size, type, uploaded date)
- Search by filename, filter by MIME type or folder
- Rename and delete assets
- Copy public URL to clipboard
- Bulk select + bulk delete
Image-specific
- Automatic thumbnail generation at upload time
- Width × height metadata stored
- Basic transforms via URL query params:
?w=800&h=600&fit=cover(proxied through a plugin endpoint that calls Sharp or similar)
Picker Component
-
<MediaPicker>modal / drawer — drop-in replacement for everyuploadImageoverride - Returns a selected asset URL
- Can be used outside a plugin route (e.g. embedded in Blog or CMS forms)
- Supports single or multi-select
Storage Adapters
- Local filesystem (for dev / self-hosted)
- S3-compatible (AWS S3, Cloudflare R2, MinIO)
- Pluggable adapter interface for other providers
Schema
import { createDbPlugin } from "@btst/stack/plugins/api"
export const mediaSchema = createDbPlugin("media", {
asset: {
modelName: "asset",
fields: {
filename: { type: "string", required: true },
originalName:{ type: "string", required: true },
mimeType: { type: "string", required: true },
size: { type: "number", required: true }, // bytes
url: { type: "string", required: true }, // public URL
thumbnailUrl:{ type: "string", required: false },
width: { type: "number", required: false },
height: { type: "number", required: false },
folderId: { type: "string", required: false },
alt: { type: "string", required: false },
createdAt: { type: "date", defaultValue: () => new Date() },
},
},
folder: {
modelName: "folder",
fields: {
name: { type: "string", required: true },
parentId: { type: "string", required: false },
createdAt: { type: "date", defaultValue: () => new Date() },
},
},
})Plugin Structure
src/plugins/media/
├── db.ts
├── types.ts
├── schemas.ts
├── query-keys.ts
├── client.css
├── style.css
├── api/
│ ├── plugin.ts # defineBackendPlugin — upload, list, delete endpoints
│ ├── getters.ts # listAssets, getAssetById, listFolders
│ ├── mutations.ts # createAsset, deleteAsset, createFolder
│ ├── query-key-defs.ts
│ ├── serializers.ts
│ ├── storage-adapter.ts # StorageAdapter interface
│ ├── adapters/
│ │ ├── local.ts # LocalStorageAdapter (writes to /public/uploads)
│ │ └── s3.ts # S3StorageAdapter (AWS / R2 / MinIO)
│ └── index.ts
└── client/
├── plugin.tsx # defineClientPlugin — library route
├── overrides.ts # MediaPluginOverrides
├── index.ts
├── hooks/
│ ├── use-media.tsx # useAssets, useAsset, useFolders
│ └── index.tsx
└── components/
├── media-picker.tsx # <MediaPicker> modal — embeddable in any plugin
└── pages/
├── library-page.tsx / .internal.tsx # Main media library UI
└── upload-zone.tsx # Drag-and-drop uploader
Routes
| Route | Path | Description |
|---|---|---|
library |
/media |
Full media library UI |
Storage Adapter Interface
export interface StorageAdapter {
upload(file: File | Buffer, options: {
filename: string
mimeType: string
folder?: string
}): Promise<{ url: string; thumbnailUrl?: string; width?: number; height?: number }>
delete(url: string): Promise<void>
}
// Built-in adapters:
export function localAdapter(options?: { uploadDir?: string; publicPath?: string }): StorageAdapter
export function s3Adapter(options: {
bucket: string
region: string
accessKeyId: string
secretAccessKey: string
endpoint?: string // for R2 / MinIO
publicBaseUrl?: string
}): StorageAdapterMediaPicker Component
The key consumer-facing component — a modal that wraps the full library UI for inline asset selection:
import { MediaPicker } from "@btst/stack/plugins/media/client"
// Drop into any form or editor
<MediaPicker
apiBaseURL="https://example.com"
apiBasePath="/api/data"
accept={["image/*"]} // optional MIME filter
multiple={false}
onSelect={(assets) => {
form.setValue("coverImage", assets[0].url)
}}
trigger={<Button>Choose image</Button>}
/>Integration with Other Plugins
Replace the scattered uploadImage override across Blog, CMS, and UI Builder:
// Before: each plugin required its own uploadImage override
blog: { uploadImage: async (file) => myCustomUpload(file) }
cms: { uploadImage: async (file) => myCustomUpload(file) }
// After: configure once, used everywhere
media: mediaClientPlugin({ apiBaseURL, apiBasePath, queryClient })
// blog/cms/ui-builder pick up MediaPicker automatically when media plugin is registeredBackend API Surface
const assets = await myStack.api.media.listAssets({ folderId: "folder-id", mimeType: "image/" })
const asset = await myStack.api.media.getAssetById("asset-id")
const folders = await myStack.api.media.listFolders()Hooks
mediaBackendPlugin({
storageAdapter: s3Adapter({ ... }),
onBeforeUpload?: (file, ctx) => Promise<void> // throw to reject (e.g. size/type validation)
onAfterUpload?: (asset, ctx) => Promise<void>
onBeforeDelete?: (asset, ctx) => Promise<void>
maxFileSizeBytes?: number // default: 10 MB
allowedMimeTypes?: string[] // default: all
})SSG Support
Media library is auth-gated — prefetchForRoute is not applicable. dynamic = "force-dynamic" on the library page.
Non-Goals (v1)
- Video transcoding / streaming
- Image CDN / edge transforms (URL param transforms are server-proxied only)
- Advanced DAM features (version history, approval workflows)
- External integrations (Unsplash, Getty)
- Per-asset access control
Plugin Configuration Options
| Option | Type | Description |
|---|---|---|
storageAdapter |
StorageAdapter |
Where files are stored (local, S3, R2) |
maxFileSizeBytes |
number |
Upload size limit (default: 10 MB) |
allowedMimeTypes |
string[] |
MIME type allowlist (default: all) |
hooks |
MediaPluginHooks |
onBeforeUpload, onAfterUpload, onBeforeDelete |
Documentation
Add docs/content/docs/plugins/media.mdx covering:
- Overview — centralized asset management, replaces scattered
uploadImageoverrides - Setup —
mediaBackendPluginwith storage adapter,mediaClientPlugin - Storage adapters — local (dev) and S3/R2 examples; custom adapter interface
<MediaPicker>component — embedding in forms, Blog, CMS, UI Builder- Integration with other plugins — how to wire up shared image picking
- Schema reference —
AutoTypeTablefor config + hooks - Image transforms — URL param reference (
?w,?h,?fit)
Related Issues
- Job Board Plugin #58 Job Board Plugin
- Calendar Booking Plugin #40 Calendar Booking Plugin
- CRM Plugin #76 CRM Plugin