Better Media
Modular media pipeline framework for intake, validation, processing, and storage.
Architecture
Core defines contracts. Adapters implement infrastructure. Framework orchestrates.
| Layer | Package(s) | Responsibility |
|---|---|---|
| Core | @better-media/core |
Interfaces only (StorageAdapter, DatabaseAdapter, JobAdapter, PipelinePlugin). No implementations. |
| Adapters | @better-media/adapter-storage-memory, @better-media/adapter-storage-filesystem, @better-media/adapter-storage-s3, @better-media/adapter-db-memory, @better-media/adapter-db-kysely, @better-media/mongodb-adapter, @better-media/adapter-jobs |
Implement core contracts (MemoryStorageAdapter, FileSystemStorageAdapter, S3StorageAdapter, etc). |
| Framework | @better-media/framework |
Orchestrate: wire adapters + plugins, run lifecycle. No infrastructure contracts or implementations. |
Monorepo Structure
packages/
├── core/ # @better-media/core - Contracts (interfaces, types)
├── better-media/ # @better-media/framework - Framework entry, lifecycle engine
├── plugins/
│ ├── validation-plugin/ # @better-media/plugin-validation
│ ├── virus-scan-plugin/ # @better-media/plugin-virus-scan
│ └── media-processing-plugin/ # @better-media/plugin-media-processing
└── adapters/
├── storages/
│ ├── storage-memory/ # @better-media/adapter-storage-memory
│ ├── storage-filesystem/ # @better-media/adapter-storage-filesystem
│ └── storage-s3/ # @better-media/adapter-storage-s3
├── databases/
│ ├── db-memory/ # @better-media/adapter-db-memory
│ ├── db-kysely/ # @better-media/adapter-db-kysely
│ └── mongodb-adapter/ # @better-media/mongodb-adapter
└── jobs/ # @better-media/adapter-jobs
Plugin System
Plugins run in either synchronous or background execution modes.
Plugin
├─ name
├─ hooks (extensible lifecycle hooks)
└─ execution mode
├─ sync – run inline during upload
└─ background – enqueue via job adapter
Job Adapter System
Background execution is powered by an optional job adapter. Default: in-memory.
- sync – Plugin runs inline during upload
- background – Plugin work is enqueued via job adapter (Redis, RabbitMQ, Kafka, etc.)
Worker Integration
Use media.runBackgroundJob(payload) from your worker process. The payload is serializable:
interface BackgroundJobPayload {
fileKey: string;
metadata: Record<string, unknown>;
hookName: HookName;
pluginName: string;
}
Example with Bull/BullMQ:
const media = createBetterMedia({ storage, database, jobs: bullAdapter, plugins });
const worker = new Worker("better-media:background", async (job) => {
await media.runBackgroundJob(job.data);
});
Example with Inngest:
inngest.createFunction(
{ id: "better-media-job" },
{ event: "better-media/background" },
async ({ event }) => {
await media.runBackgroundJob(event.data.payload);
}
);
The framework does not implement polling or scheduling—adapters and your worker own that.
Storage Adapters
Choose a storage implementation based on your environment:
| Adapter | Package | Use case |
|---|---|---|
| Memory | @better-media/adapter-storage-memory |
Development, tests |
| Filesystem | @better-media/adapter-storage-filesystem |
Single-node, local disk |
| S3 | @better-media/adapter-storage-s3 |
AWS S3, MinIO, S3-compatible |
Filesystem (works with Multer in Express/NestJS):
import { FileSystemStorageAdapter } from "@better-media/adapter-storage-filesystem";
const storage = new FileSystemStorageAdapter({ baseDir: "/var/uploads" });
S3 (AWS or MinIO):
import { S3StorageAdapter } from "@better-media/adapter-storage-s3";
const storage = new S3StorageAdapter({
region: "us-east-1",
bucket: "my-media-bucket",
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
// For MinIO:
// endpoint: "http://localhost:9000",
// forcePathStyle: true,
});
Quick Start
pnpm install
pnpm build
Scripts
| Command | Description |
|---|---|
pnpm build |
Build all packages |
pnpm dev |
Watch mode for all packages |
pnpm typecheck |
Type-check all packages |
pnpm lint |
Lint all packages |
pnpm test |
Run tests |
pnpm format |
Format with Prettier |
pnpm changeset |
Create a changeset for versioning |
Testing with Postman
A Postman collection is provided in the root directory to help you test the examples:
The collection includes:
- Multipart Uploads: Test Multer-based ingest.
- Binary Uploads: Test raw buffer ingest.
- Unified Presigned Uploads: Full flow for both PUT and POST methods (Step-by-step).
To use it:
- Import the JSON file into Postman.
- Start an example (e.g.,
cd examples/express && pnpm dev). - Use the collection variables to switch between Express (Port 6000) and NestJS (Port 3000).
Adding a Plugin
- Create
packages/plugins/<name>-plugin/withpackage.json,tsconfig.json,tsup.config.ts - Implement the
PipelinePlugininterface from@better-media/core - Add
@better-media/coreas a workspace dependency
Adding an Adapter
- Create
packages/adapters/<name>/(or add to existing storage/db) - Implement the contract from
@better-media/core(e.g.StorageAdapter,DatabaseAdapter) - Export the implementation; re-export the interface from core for convenience
- Add to workspace in
pnpm-workspace.yamlif using a new top-level adapter package