---
url: https://docs.nemo12.com/architecture/sdd-009-media.md
description: >-
  Media Service dùng chung (SDD-009): Media Registry, luồng upload, lưu ảnh trên
  R2 và bài học rút ra từ ảnh hỏng ở hệ legacy.
---

# SDD-009 — Media & Image Management

Một **Media Service dùng chung toàn Nemo12** (horizontal capability như Interaction/Notification). Bài học legacy (sutucon): ảnh base64 lưu thẳng trong D1 (`learner_product_submission_images`, `product_feedback_request_images`, ≤200KB, `hidden_at` moderation) — không scale, phình DB; `IMAGES` (Cloudflare Images) và R2 khai báo nhưng chưa dùng. Chuyenchon: 5 R2 buckets rời rạc theo app. Nemo12 làm đúng ngay từ đầu.

## 1. Kiến trúc

```text
Client → api.nemo12.com /v1/media
   ├─ D1: media registry (metadata, ownership, moderation, versions)
   ├─ R2: nemo12-content — bytes gốc, key media/<yyyy>/<id>/<filename>
   └─ Cloudflare Images (transformations): variants (thumb, card, full) qua CDN
Event: MediaUploaded / MediaModerated → Queue → moderation + AI analysis
```

## 2. Media Registry (REQ-MED-01)

```text
media: id · owner_user_id · learner_id? · target_type · target_id · kind(image|audio|video|document)
· file_name(≤160) · mime_type · byte_size · width/height · checksum_sha256 · r2_key
· visibility(private|learner_parent|mentor_team|school|community|public)
· moderation_status(pending|approved|hidden|blocked) · hidden_at · created_at · version
```

`target_type + target_id` giống Interaction (SDD-005): ảnh gắn vào submission, evidence, comment, learner report, content asset, school page… Một ảnh tái sử dụng nhiều target qua bảng `media_links` — **bảng này chưa được dựng** (tính tới 2026-08-20, `media` gắn trực tiếp một target); dùng lại nhiều nơi là thiết kế đã chốt nhưng chưa tới lượt thi hành.

## 3. Upload flow (REQ-MED-02)

1. Client `POST /v1/media/uploads` (metadata + intent) → validate type/size/quota → trả **direct upload URL** (R2 presigned hoặc Images direct upload) — bytes không đi qua Worker (tránh giới hạn CPU/body).
2. Client PUT bytes → confirm `POST /v1/media/:id/complete` → verify checksum + head object → status `pending`.
3. Event `MediaUploaded` → Queue → moderation pipeline.

Giới hạn mặc định: image ≤10MB (PNG/JPG/WebP/HEIC), document ≤25MB (PDF), audio/video theo nhu cầu sau; quota per user/ngày. Fallback nhỏ: ảnh ≤200KB được phép inline base64 một chiều để tương thích import legacy, nhưng luôn được chuyển sang R2 bởi worker nền (không lưu vĩnh viễn trong D1).

## 4. Delivery (REQ-MED-03)

* Variants qua Cloudflare Images: `thumb 160px · card 640px · full 1600px`, auto-format (AVIF/WebP).
* URL ký (signed) cho visibility ≠ public; authorization backend theo relationship (parent↔family, mentor↔assignment) trước khi cấp URL.
* Cache CDN; `content_ref` trong content system (SDD-004) trỏ media id.

## 5. Moderation & Safety — K12 (REQ-MED-04)

Mọi media learner/parent upload: `pending` → AI classify (nudity/violence/PII/inappropriate — qua AI Gateway) → auto-approve ngưỡng an toàn hoặc vào moderation queue (admin) → approve/hide/block + audit log. Soft-hide bằng `hidden_at` (pattern sutucon). EXIF strip mặc định; cấm serve ảnh chưa approved cho visibility community/public.

## 6. Lifecycle & Versioning (REQ-MED-05)

Immutable theo version; thay ảnh = version mới (giữ provenance). Retention: media mồ côi (không link) dọn sau 30 ngày; xóa theo yêu cầu chủ thể dữ liệu (child privacy — REQ-SEC-04) = hide ngay + purge R2 theo lịch, audit giữ metadata tối thiểu.

## 7. AI illustrations (REQ-MED-06 ⏳ scope)

Pipeline sinh ảnh minh họa content (kế thừa mori: flux-1-schnell + llava review) ghi vào cùng registry với `source=ai_generated`, provenance model/prompt — bật sau khi content pipeline chạy (Q-051).

## Trace

REQ-MED-01→§2 · 02→§3 · 03→§4 · 04→§5 · 05→§6 · 06→§7.
