有料ポッドキャスト配信を自前で作ったら想像以上に沼だった話
台本
オープニング
【ジングル】 はい、どうもこんにちは。「雨宿りと WEB の小噺」、始まりました。Keeth こと桑原です。 今回もちょっとだけ、雨宿りしていきませんか。 今回のお題はですね、「有料ポッドキャスト配信を自前で作ったら想像以上に沼だった話」。ちょっと面白い話なんで、まぁ聞いてってください。
本題
そもそも何をやろうとしたか
- この番組にプレミアムプラン(メンバーシッププラン)を作ろうとしている
- 自分のポッドキャスト公式サイト(Riot.js製のSPA)に、メンバーだけが聴けるプレミアムエピソードを追加する構想
- 「外部サービスに頼らず、フルスクラッチで作ってみたい」という個人開発欲もあり、自作することに
- 現在の配信基盤はArt19。そのAlternate Feed(限定フィード)機能を使ってプレミアムエピソードを配信する前提
フェーズ1:シンプルに2つのフィードをマージすればいいんじゃない?
- 通常フィードとAlt Feedを
Promise.allSettled()で両方取得して時系列マージ、で終わるかと思っていた - 壁1:Art19の埋め込みプレイヤー(iframeベース)が、Alternate Feedのエピソードに対して404を返す
- 仕方なくHTML5の
<audio>タグで直接再生する方式に切り替え - 壁2:
<audio>タグで再生するにはRSSの<enclosure>タグから音声URLを直接取得する必要がある → Alternate FeedのURLがクライアントに丸見え - URLを知っていれば誰でもアクセスできてしまうので、この方式はボツ
- 通常フィードとAlt Feedを
フェーズ2:サーバーサイドに逃がせばURLを隠せる
- Firebase Cloud Functionsにフィード取得処理を移し、クライアントからはオーディオURLを直接見せない構成に
- Cloud Functions側でFirestoreを参照し、「プレミアムユーザかどうか」の認証チェックも実装
- 壁3:Cloud Functionsから返された音声URLをユーザがコピーして共有したら意味がない
- URLが漏れた瞬間に誰でもアクセスできてしまう問題は解消できず、またボツ
ここで外部サービスを真剣に比較してみた
- Memberful:月額$25固定。ユーザーごとに固有のハッシュ付きRSSを発行してくれるのが魅力。でも月額固定コストが高い
- Supercast:月額固定なし、取引額の5.5% + Stripe手数料。ユーザーごとのRSS発行もある。手数料率が惜しい
- Apple Podcasts:年額$19.99 + 決済手数料25〜30%。Apple独自配信方式に縛られるのも悩ましい
- Spotify:月額・決済手数料ともにゼロ。ただし自分の配信基盤(Art19 → 各プラットフォームへRSS連携)と仕組みがバッティングしてしまう
- note:メンバーシップで音声を載せる方法もあり、一番手っ取り早い。手数料は取られるが将来的に検討の余地あり
- Rebuild.fm:ウェブポータル経由で完全に振り切ったスタイル。Backspace.fm:プレミアムフィードをしっかり運用している
- 結論:ユーザーごとのRSSが発行できるSupercastが現実的。でも手数料が気になって踏み切れず…
フェーズ3:Art19の認証機能を調べてみた
- Art19には「共有トークン」をURLのクエリパラメーターに付与する方式がある
- 問題:全ユーザー共通のシークレットなので、1人に漏れたら全員アウト。ローテーションしても登録済みリスナーのフィードが無効になる
- もう一つの方式:
Authorization: Bearer {token}ヘッダーを使う方法 - 問題:
<audio>タグやポッドキャストアプリはHTTPリクエストに任意のヘッダーを付与できない - 結論:どちらの方式でも「ポッドキャストアプリが直接アクセスできる」という要件を満たせない → プロキシが必要という結論に
フェーズ4(最終形):Cloudflare Workers で認証プロキシを自作
- Cloudflare Workersを選んだ理由:コールドスタートがほぼゼロ、音声ストリーミング(Range Header)への対応、無料枠が十分
- Firebase Functions、Lambda、Dynamoなどとも比較したが、レスポンス速度・コスト・ストリーミング対応でWorkersに軍配
- アーキテクチャは2段構成:
- Firebase:ユーザー認証 + Stripeによる課金管理(Firestoreに課金状態を保持)
- Cloudflare Workers + KV:リクエストごとの認可チェック(KVへの読み取りは数ms)
- 署名付きURL + KVの2重チェックで音声へのアクセスを制御
- 署名が有効でも、KVから削除されていればアクセス不可 → 共有されても即時無効化できる
- StripeのWebhookを起点にFirestoreとCloudflare KVの両方に書き込むイベント駆動パターン
- 音声プロキシはRangeヘッダーを透過的に転送し、206(Partial Content)のままポッドキャストアプリに返す
現時点での積み残しと今後の課題
- Art19の埋め込みプレイヤーがAlt Feedで使えないため、プレミアムエピソードはシンプルな
<audio>タグのみ - ポッドキャストアプリへの自動登録(Supercastのようなワンタップ追加)がまだ未実装。
podcast://やovercast://スキームの活用を今後検討 - Cloudflare Workers経由だとArt19側のリスナー統計が正しく記録されない可能性があるため、独自ログの取得も将来的に必要
- 署名付きURLはパラメーターが毎回変わるためCDNキャッシュとの相性が悪く、キャッシュ設計が課題
- Art19の埋め込みプレイヤーがAlt Feedで使えないため、プレミアムエピソードはシンプルな
学びと結論
- 「フィードを2つマージするだけ」のつもりが、セキュリティを真剣に考え始めると芋づる式に問題が出てきた
- 最大の誤算:
<audio>タグはAuthorizationヘッダーを送れない → これがわかった瞬間にブラウザ経由での直接認証はすべて不可能になり、プロキシ一択に - Firebase Firestore(本体データ)とCloudflare KV(認可キャッシュ)の役割分担は試行錯誤の末にたどり着いた設計
- 最終的な教訓:フォロワーが少ないうちは素直にSupercastを使いましょう。自前実装は学びにはなるが、Supercastなら手数料を払うだけで全部解決する
エンディング
【ジングル】 さて、そろそろ今回もお時間です。 面白かったよーという方は、ぜひチャンネル登録もお願いします。話してほしいトピックや感想は、概要欄のフォームか 𝕏 で「WEB 小噺」でつぶやいてください。web はアルファベット、「小噺」は漢字でもひらがなでも大丈夫です! それでは、また雨宿りしに来てください。お相手は Keeth でした。さようなら! 【ジングル】