248 lines
11 KiB
Markdown
248 lines
11 KiB
Markdown
# Media Storage Decoupling + MinIO Support PRD
|
|||
|
|
|
||
|
|
## Purpose
|
||
|
|
Decouple ClassroomIO media storage from Cloudflare-specific infrastructure so open-source and self-hosted users can control their own media storage backend, with first-class MinIO support.
|
||
|
|
|
||
|
|
## Problem Statement
|
||
|
|
The current media pipeline is tightly coupled to Cloudflare R2 and Cloudflare Browser Rendering APIs. This creates friction for open-source adopters who want full infrastructure ownership, local/private deployments, and non-Cloudflare object storage.
|
||
|
|
|
||
|
|
Current tight-coupling points:
|
||
|
|
- `apps/api/src/utils/s3.ts` hardcodes Cloudflare R2 endpoint and credentials.
|
||
|
|
- `apps/api/src/services/media.ts` requires `CLOUDFLARE_IMAGE_BUCKET_DOMAIN` for image URLs.
|
||
|
|
- `apps/api/src/routes/course/presign.ts` directly uses Cloudflare-bound storage utilities.
|
||
|
|
- `apps/api/src/utils/lesson-media.ts` directly generates download URLs using Cloudflare-bound utilities.
|
||
|
|
- `apps/api/src/config/env.ts` and `apps/api/src/constants/index.ts` are Cloudflare-first.
|
||
|
|
- Docker and docs surface only `CLOUDFLARE_*` storage setup paths.
|
||
|
|
|
||
|
|
## Goals
|
||
|
|
1. Introduce provider-based object storage architecture in API code.
|
||
|
|
2. Preserve current dashboard and API contracts for uploads/downloads.
|
||
|
|
3. Support Cloudflare R2 through a generic S3-compatible provider.
|
||
|
|
4. Add MinIO as a required, documented, self-hosted storage option.
|
||
|
|
5. Keep backward compatibility for existing Cloudflare-based deployments.
|
||
|
|
6. Maintain canonical asset metadata and usage tracking without regressions.
|
||
|
|
|
||
|
|
## Non-Goals (Initial Release)
|
||
|
|
- Migrating away from presigned URL upload/download flows.
|
||
|
|
- Replacing current video transcoding strategy.
|
||
|
|
- Fully decoupling PDF generation from Cloudflare in the same release.
|
||
|
|
- Per-organization storage-provider selection (initial scope is instance-level).
|
||
|
|
|
||
|
|
## Requirements
|
||
|
|
|
||
|
|
### Functional Requirements
|
||
|
|
1. API storage access must go through a provider abstraction layer, not Cloudflare-specific helpers.
|
||
|
|
2. Existing presign API endpoints and response shapes must remain unchanged.
|
||
|
|
3. Existing media image upload endpoint response shape must remain unchanged.
|
||
|
|
4. Download URL generation for lessons/documents must remain transparent to frontend consumers.
|
||
|
|
5. Existing Cloudflare deployments must continue working with no required immediate migration.
|
||
|
|
|
||
|
|
### MinIO Requirements (Must-Have)
|
||
|
|
1. MinIO support is required in the first release of this decoupling effort.
|
||
|
|
2. MinIO must work with direct browser uploads via presigned URLs.
|
||
|
|
3. MinIO must support three buckets equivalent to current model:
|
||
|
|
- `videos`
|
||
|
|
- `documents`
|
||
|
|
- `media`
|
||
|
|
4. MinIO path-style compatibility must be supported.
|
||
|
|
5. Docker self-host flow must include an option to run MinIO locally.
|
||
|
|
6. Self-host docs must include complete MinIO setup steps:
|
||
|
|
- bucket creation
|
||
|
|
- credentials
|
||
|
|
- CORS rules
|
||
|
|
- required env vars
|
||
|
|
- verification steps
|
||
|
|
|
||
|
|
### Backward Compatibility Requirements
|
||
|
|
1. If new storage env vars are absent and `CLOUDFLARE_*` vars are present, runtime must auto-map to Cloudflare-compatible mode.
|
||
|
|
2. Existing frontend flows must not require code changes for basic upload/download behavior.
|
||
|
|
3. Existing `storageProvider` values in assets should continue to validate and persist.
|
||
|
|
|
||
|
|
### Non-Functional Requirements
|
||
|
|
1. No material latency regression for presign endpoints.
|
||
|
|
2. Error messages should be provider-agnostic and actionable.
|
||
|
|
3. Security posture must remain consistent:
|
||
|
|
- short-lived presigned URLs
|
||
|
|
- content-type restrictions
|
||
|
|
- file-size validation
|
||
|
|
- principle of least privilege for storage credentials
|
||
|
|
|
||
|
|
## Current State Audit
|
||
|
|
|
||
|
|
| Area | Current State | Gap | Priority |
|
||
|
|
| --- | --- | --- | --- |
|
||
|
|
| Object storage client | Cloudflare R2 hardcoded | No provider abstraction | P0 |
|
||
|
|
| Presigned URLs | Cloudflare-bound utility | Cannot swap provider | P0 |
|
||
|
|
| Image upload public URL | Cloudflare domain env only | No generic public URL strategy | P0 |
|
||
|
|
| Env config | `CLOUDFLARE_*` only | No generic object storage config | P0 |
|
||
|
|
| Docker self-host | No MinIO service path | Hard for OSS users to self-own media | P0 |
|
||
|
|
| Docs | Cloudflare-first guidance | Missing MinIO-first self-host path | P0 |
|
||
|
|
| Asset uniqueness constraint | Unique key excludes `storageProvider` | Potential collision if multiple providers introduced later | P1 |
|
||
|
|
| PDF rendering | Cloudflare-specific browser rendering API | Separate Cloudflare dependency remains | P2 |
|
||
|
|
|
||
|
|
## Proposed Architecture
|
||
|
|
|
||
|
|
### 1) Storage Provider Abstraction
|
||
|
|
Create a provider interface under API utils/services layer.
|
||
|
|
|
||
|
|
Suggested interface responsibilities:
|
||
|
|
- `uploadObject`
|
||
|
|
- `getObject`
|
||
|
|
- `deleteObject`
|
||
|
|
- `createUploadPresignedUrl`
|
||
|
|
- `createDownloadPresignedUrl`
|
||
|
|
- `buildPublicUrl`
|
||
|
|
|
||
|
|
### 2) S3-Compatible Provider
|
||
|
|
Implement a single S3-compatible provider using AWS SDK v3, configured by env.
|
||
|
|
|
||
|
|
This provider should support:
|
||
|
|
- Cloudflare R2
|
||
|
|
- MinIO
|
||
|
|
- AWS S3
|
||
|
|
- Other S3-compatible object stores
|
||
|
|
|
||
|
|
### 3) Config Model
|
||
|
|
Add provider-neutral env vars in API config:
|
||
|
|
- `OBJECT_STORAGE_DRIVER` (initial values: `s3-compatible`)
|
||
|
|
- `OBJECT_STORAGE_REGION`
|
||
|
|
- `OBJECT_STORAGE_ENDPOINT`
|
||
|
|
- `OBJECT_STORAGE_PUBLIC_ENDPOINT` (optional; used for browser-facing presigned URLs where needed)
|
||
|
|
- `OBJECT_STORAGE_ACCESS_KEY_ID`
|
||
|
|
- `OBJECT_STORAGE_SECRET_ACCESS_KEY`
|
||
|
|
- `OBJECT_STORAGE_FORCE_PATH_STYLE` (important for MinIO compatibility)
|
||
|
|
- `OBJECT_STORAGE_BUCKET_VIDEOS`
|
||
|
|
- `OBJECT_STORAGE_BUCKET_DOCUMENTS`
|
||
|
|
- `OBJECT_STORAGE_BUCKET_MEDIA`
|
||
|
|
- `OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL`
|
||
|
|
- `OBJECT_STORAGE_PRESIGN_UPLOAD_EXPIRES_SECONDS`
|
||
|
|
- `OBJECT_STORAGE_PRESIGN_DOWNLOAD_EXPIRES_SECONDS`
|
||
|
|
|
||
|
|
Backward-compatible fallback mapping from existing vars:
|
||
|
|
- `CLOUDFLARE_ACCOUNT_ID`
|
||
|
|
- `CLOUDFLARE_ACCESS_KEY`
|
||
|
|
- `CLOUDFLARE_SECRET_ACCESS_KEY`
|
||
|
|
- `CLOUDFLARE_BUCKET_DOMAIN`
|
||
|
|
- `CLOUDFLARE_IMAGE_BUCKET_DOMAIN`
|
||
|
|
|
||
|
|
### 4) API Contract Stability
|
||
|
|
Keep these contracts unchanged:
|
||
|
|
- `POST /course/presign/video/upload`
|
||
|
|
- `POST /course/presign/document/upload`
|
||
|
|
- `POST /course/presign/video/download`
|
||
|
|
- `POST /course/presign/document/download`
|
||
|
|
- `POST /media/image`
|
||
|
|
|
||
|
|
### 5) Data Model Considerations
|
||
|
|
Current assets schema default `storage_provider='s3'` is acceptable for initial rollout.
|
||
|
|
If multi-provider-per-instance or migration coexistence is introduced, consider updating unique constraint:
|
||
|
|
- from `(organization_id, provider, storage_key)`
|
||
|
|
- to `(organization_id, provider, storage_provider, storage_key)`
|
||
|
|
|
||
|
|
## Complexity Assessment
|
||
|
|
|
||
|
|
| Workstream | Complexity | Why |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| Storage abstraction refactor (`apps/api`) | High | Core upload/download paths and presign generation are central and cross-cutting |
|
||
|
|
| MinIO support in presign/browser flows | High | Requires endpoint/path-style/CORS correctness across API and browser contexts |
|
||
|
|
| Env/config migration with backward compatibility | Medium | Needs careful fallback behavior and clear deprecation strategy |
|
||
|
|
| Docker self-host MinIO integration | Medium | Additional service wiring, bucket bootstrap, and docs updates |
|
||
|
|
| Dashboard compatibility | Low | Existing frontend contracts can remain unchanged if API contracts are preserved |
|
||
|
|
| DB uniqueness hardening for future multi-provider | Medium | Requires migration planning and index/constraint rollout |
|
||
|
|
| Testing and regression coverage | High | Upload/download flows span API, browser, storage service, and content rendering paths |
|
||
|
|
| PDF rendering decoupling | Out of scope in this release | Separate dependency track and broader decision surface |
|
||
|
|
|
||
|
|
## Implementation Plan
|
||
|
|
|
||
|
|
### Phase 0: Foundation
|
||
|
|
1. Introduce provider interface and s3-compatible provider implementation.
|
||
|
|
2. Replace direct imports from Cloudflare-bound storage utility across:
|
||
|
|
- `apps/api/src/services/media.ts`
|
||
|
|
- `apps/api/src/routes/course/presign.ts`
|
||
|
|
- `apps/api/src/utils/lesson-media.ts`
|
||
|
|
3. Implement provider-neutral config in env schema and constants.
|
||
|
|
4. Add Cloudflare fallback mapping and warning logs.
|
||
|
|
|
||
|
|
### Phase 1: MinIO Required Path
|
||
|
|
1. Add MinIO configuration support in API.
|
||
|
|
2. Add/extend Docker compose for optional MinIO service.
|
||
|
|
3. Add bucket bootstrap instructions (videos/documents/media).
|
||
|
|
4. Add CORS guidance specific to browser presigned PUT/GET/HEAD behavior.
|
||
|
|
|
||
|
|
### Phase 2: Documentation + Hardening
|
||
|
|
1. Update self-host docs with MinIO-first workflow.
|
||
|
|
2. Update Cloudflare docs to show Cloudflare as one provider option.
|
||
|
|
3. Update `apps/api/.env.example` and docker env templates.
|
||
|
|
4. Add integration smoke tests for upload/download and media image URL generation.
|
||
|
|
|
||
|
|
### Phase 3: Optional Follow-Up
|
||
|
|
1. Evaluate schema unique constraint expansion to include `storage_provider`.
|
||
|
|
2. Evaluate PDF rendering provider abstraction (Cloudflare-independent renderer).
|
||
|
|
|
||
|
|
## Required File Touchpoints
|
||
|
|
|
||
|
|
Core API:
|
||
|
|
- `apps/api/src/config/env.ts`
|
||
|
|
- `apps/api/src/constants/index.ts`
|
||
|
|
- `apps/api/src/utils/s3.ts` (or replacement provider module)
|
||
|
|
- `apps/api/src/services/media.ts`
|
||
|
|
- `apps/api/src/routes/course/presign.ts`
|
||
|
|
- `apps/api/src/utils/lesson-media.ts`
|
||
|
|
|
||
|
|
Docs and self-host:
|
||
|
|
- `apps/docs/content/docs/quickstart/self-hosting.mdx`
|
||
|
|
- `apps/docs/content/docs/contributor-guides/cloudflare-setup.mdx`
|
||
|
|
- `apps/api/.env.example`
|
||
|
|
- `docker/docker-compose.yaml`
|
||
|
|
- `docker/docker-compose.prod.yaml`
|
||
|
|
- `docker/.env.prod.example`
|
||
|
|
- `docker/docs/USAGE.md`
|
||
|
|
- `docker/docs/SELF_HOST.md`
|
||
|
|
|
||
|
|
Optional DB follow-up:
|
||
|
|
- `packages/db/src/schema.ts`
|
||
|
|
- corresponding migration files
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
### Unit Tests
|
||
|
|
1. Provider config parsing and fallback behavior.
|
||
|
|
2. Presigned URL generation for upload/download.
|
||
|
|
3. Public URL generation for media bucket objects.
|
||
|
|
|
||
|
|
### Integration Tests
|
||
|
|
1. Presign video upload endpoint returns valid URL + key.
|
||
|
|
2. Presign document upload endpoint returns valid URL + key.
|
||
|
|
3. Presign download endpoints return URL map for provided keys.
|
||
|
|
4. Media image upload endpoint uploads and returns public URL.
|
||
|
|
|
||
|
|
### Manual Verification Matrix
|
||
|
|
1. Cloudflare R2 existing env-only setup remains functional.
|
||
|
|
2. MinIO self-host setup works end-to-end for:
|
||
|
|
- video upload
|
||
|
|
- document upload
|
||
|
|
- thumbnail/image upload
|
||
|
|
- lesson playback/download link resolution
|
||
|
|
|
||
|
|
## Risks and Mitigations
|
||
|
|
|
||
|
|
| Risk | Impact | Mitigation |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| Presigned URL host mismatch in containerized MinIO setups | Upload/download failures in browser | Support public endpoint config and document deployment topology clearly |
|
||
|
|
| CORS misconfiguration in MinIO buckets | Browser request failures | Provide explicit CORS examples and startup checklist |
|
||
|
|
| Hidden reliance on Cloudflare-specific behavior | Runtime regressions | Add compatibility tests and phased rollout with fallback |
|
||
|
|
| Config ambiguity between old and new env vars | Misconfiguration risk | Define clear precedence, validation, and startup logs |
|
||
|
|
| Future multi-provider collisions in assets uniqueness | Data integrity risk | Plan optional schema hardening phase with migration |
|
||
|
|
|
||
|
|
## Acceptance Criteria
|
||
|
|
1. A self-hosted deployment can use MinIO without any Cloudflare credentials.
|
||
|
|
2. Existing Cloudflare deployments continue to work without immediate config rewrite.
|
||
|
|
3. Dashboard upload/playback/download behavior remains unchanged for users.
|
||
|
|
4. Docs provide a complete MinIO setup path from zero to working upload/playback.
|
||
|
|
5. Docker self-host users can run a documented MinIO-backed stack.
|
||
|
|
|
||
|
|
## Open Questions
|
||
|
|
1. Should we support two endpoints in config by default (`internal` for API operations, `public` for presigned browser URLs), or require one endpoint reachable by both?
|
||
|
|
2. Do we want to enforce a strict enum for `storageProvider` in validation now, or keep it flexible string-based for compatibility?
|
||
|
|
3. Should PDF rendering decoupling be planned as a separate PRD immediately after this release?
|