YiBi Dev Docs
架構決策 (ADR)

媒體儲存雙軌策略 — 精選內容用 Cloudflare R2 + CDN;UGC 用 GCS Signed URL

Context

媒體(音訊 MP3)儲存策略目前沒有 ADR 記錄。決策實際上在 2026-05 已做成,但只埋在已歸檔的 Spectra change docs/openspec/changes/archive/2026-05-04-media-delivery-cdn-strategy/ (cap ip-content-r2-storage,後併入 E15-ugc-playback)的 design.md 決策二裡。L3 DECISION 應該在 docs/adr/ 有一個可發現的家,這份 ADR 回溯記錄該決策,並補上一個當初沒做的成本比較 (R2 vs GCS + Google Cloud CDN;原 design 只比了 R2 vs GCS + Cloudflare CDN)。

系統有兩類音訊內容,存取特性截然不同(已由 contents.source_type 欄位編碼):

  • UGC / 個人化source_type IN (ai_generated, ai_custom)):每個孩子的 AI 生成故事/兒歌 (ai_generated)與 E18 Custom Creator 自製育兒內容(ai_custom,content_generation.py 帶 family_id/ child_profile_id)。皆 family 私有、唯一、重用率極低(含孩子名字,CDN cache hit 估 < 5%)。
  • YiBi 精選 / IPsource_type IN (curated, licensed)):Admin portal 產生或授權的故事/音樂, 跨家庭共享、高重用。

contents.source_type 共四個值(見 E11-device-token spec):curated / licensed / ai_generated / ai_custom。前兩者公開共享,後兩者 family 私有——這條公開/私有界線正是下方雙軌的依據。

現況:production 全部走 GCS(Terraform 寫死 STORAGE_PROVIDER=gcs)。R2 provider 已實作但尚未上線; 切換前還有 bug(issue #368:R2 upload() 回傳 CDN URL 而非 object key,導致 delete 靜默失敗)待修。 本 ADR 記錄的是目標策略,非當前 production 狀態。

Decision

媒體儲存依 source_type 分兩軌:

內容類別source_type儲存 + 交付URL 形式
YiBi 精選 / IP(公開共享)curated, licensedCloudflare R2 + Cloudflare CDN公開 CDN URL(build_ip_content_path() 路徑)
UGC(family 私有)ai_generated, ai_customGCS + Signed URL過期簽章 URL(預設 1h TTL)

隱私邊界鐵則:路由依據是「公開共享 vs family 私有」,不是「AI 生成 vs 人工」。任何帶 family_id 的內容(含 ai_custom)一律走私有 GCS Signed URL 軌,不得放上公開 R2/CDN,無論 egress 成本。

選擇依據有兩條獨立的軸,兩者都指向同一個雙軌結論:

軸一:成本(egress 主導)

單位價格(2026-06;Google 以 GiB 計價,≈1.074 GB,下表已大致正規化為 GB 比較):

成本項R2 + Cloudflare CDNGCS + Google Cloud CDNGCS 直連 Signed URL
儲存 /GB-月$0.015$0.020$0.020
對使用者 egress /GB$0(免費)~$0.08(cache egress,首 10 TB NA/EU)~$0.12(首 1 TB)
Cache fill /GB~$0.01
請求 opsClass B $0.36/Mcache lookup $0.75/M + cache miss 時 GCS opsClass B $0.40/M
固定成本HTTPS Load Balancer ~$18/月

精選內容(目錄 ~2 GB)兩情境試算:

每月 egressR2 + CDNGCS + Cloud CDNGCS 直連
120 GB(~1k 裝置)< $0.5~$28~$14
1,200 GB(~10k 裝置)< $0.5~$115~$144
  • R2 因 egress 免費,成本幾乎不隨流量變動(只剩儲存 + 讀取 ops);Google 兩案都隨 egress 線性成長。
  • 10k 裝置規模下,R2 相對 Google 兩案省 ~$115–144/月,且隨裝置數線性放大。
  • Crossover 提醒:GCS + Cloud CDN 要 egress > ~450 GB/月才贏 GCS 直連(因為 LB $18/月固定成本
    • cache egress $0.08 vs 直連 $0.12 的差額需要足夠流量才回本)。低流量時加 Google CDN 反而更貴。

軸二:存取控制(決定性,非成本可取代)

R2 這套設計回傳的是公開 CDN URL,無 per-request 認證backend/src/infrastructure/providers/storage/r2.pyget_url() 回傳 {CDN_BASE_URL}/{path}backend/src/infrastructure/config/signed_url.py 對 R2 URL 直接 pass-through 不簽章)。

  • 精選內容公開共享 → 公開 URL 完全合適,且能吃到 CDN 邊緣快取。
  • UGC 是 family-scoped 私有資料 → 必須用簽章 + 過期 URL 做存取控制,不能放在公開 CDN bucket。

因此即使 R2 egress 免費,私有 UGC 也不適合走目前的 R2 公開路徑;保留在 GCS Signed URL 是存取控制的要求, 不只是快取經濟。contents.source_type 已把這條公開/私有界線編碼進資料層。

Alternatives Considered

A. 全部走 GCS + Google Cloud CDN

  • 優點:單一雲(GCP),統一 IAM / 可觀測性;Signed URL 原生支援,私有內容沒問題。
  • 缺點:egress 不免費(cache egress ~$0.08/GB)成本線性成長;HTTPS LB ~$18/月固定成本; 低流量(< ~450 GB/月)時 CDN 反而比 GCS 直連貴;UGC cache hit < 5%,對 UGC 加 CDN 是純負擔。
  • 未採用:精選內容的 egress 成本顯著高於 R2;對 UGC 無快取收益。

B. 全部走 GCS 直連 Signed URL(= 現況 production)

  • 優點:最簡單,單一系統,私有/公開都用簽章 URL 統一處理。
  • 缺點:egress 最貴(~$0.12/GB),無快取層,origin 服務每一個 byte;精選內容跨家庭重複下載時成本爆炸。
  • 未採用(對精選內容):高重用共享內容不放 CDN 是把錢丟進 egress;保留作為 UGC 的軌道。

C. 全部走 R2 + Cloudflare CDN(含 UGC)

  • 優點:egress 全免費,成本最低。
  • 缺點:R2 公開 CDN URL 無法安全服務私有 UGC(見軸二)。
  • 未採用:違反 UGC 的存取控制需求。

Consequences

  • 正面:精選內容 egress 成本扁平、可預測,隨裝置量成長不爆炸;UGC 維持簽章存取控制。
  • 代價(接受)
    • 跨雲營運面:精選儲存在 Cloudflare R2,其餘基礎設施在 GCP(兩套憑證/監控/帳務)。
    • 公開 URL 限制:R2 軌道只能放公開內容;任何需要 per-request 授權的內容不得進此軌。
    • 兩套 provider 需各自維運(STORAGE_PROVIDER 切換 + CDN_BASE_URL 設定)。
  • 實作前置(R2 上線前必做)
    • 修 issue #368(R2 upload() 改回傳 object key,signed_url R2 分支組 CDN URL,factory 加 CDN_BASE_URL guard)。
    • R2 路徑沿用既有 build_ip_content_path()v{version}/ip/{type-plural}/{id}/audio.mp3,type 段已複數化: story → storiesmusic → music,見 r2.py:16-23)。注意此函式已存在但尚未接上 curated 上傳—— 目前 curated 音訊上傳走 curated/{id}.mp3content_generation.py),遷移 R2 時才改套用。
    • 上線追蹤見 issue #369(音訊儲存遷移 R2 + CDN)。

Follow-up / Open Question(不在本 ADR 決策範圍,僅記錄)

UGC 目前付全額 GCS egress(~$0.12/GB)。理論上 UGC 改用 R2 S3 presigned URL(不走 CDN) 也能拿到免費 egress 又保住存取控制(presigned URL 有過期,等同簽章)。代價是多一條 R2 私有路徑 + presign 邏輯,且失去 GCS 既有的 IAM signBlob 整合。記為未來優化,待 UGC egress 量體成長、或 R2 軌道穩定後再評估,不在本決策內。

  • 原始決策(Spectra change,archived):docs/openspec/changes/archive/2026-05-04-media-delivery-cdn-strategy/design.md(決策二)
  • 相關 spec:docs/openspec/specs/E15-ugc-playback/
  • R2 上線實作:issue #368(delete bug)、issue #369(遷移 R2 + CDN)
  • 架構總覽:docs/architecture/system-overview.md(§2.4 儲存層)
  • 定價來源:Cloudflare R2 pricingGCS pricingGoogle Cloud CDN pricing

On this page