メンバーシップ限定配信の基盤を自前で作ったら沼すぎて、最終的に全部捨てた話
配信ステータス:未配信 配信URL:(配信済みになったら記入)
台本
オープニング
【ジングル】 はい、どうもこんにちは。「雨宿りと WEB の小噺」、始まりました。Keeth こと桑原です。 今回もちょっとだけ、雨宿りしていきませんか。 今回のお題はですね、「有料ポッドキャスト配信を自前で作ったら沼すぎて、最終的に全部捨てた話」。設計して、実装して、最後に全部捨てるまでの一部始終です。 先に一言だけ。今回は途中からわりとガッツリめにエンジニア向けの話になります。専門用語も出てきますが、その都度できるだけかみ砕いて説明するので、非エンジニアの方も「へー、そんな苦労してるんだ」くらいの温度感で聞いてもらえたら嬉しいです。まぁ聞いてってください。
本題
そもそも何をやろうとしたか
- この番組にプレミアムプラン(メンバーシッププラン)を作ろうとしている
- やりたかったことは4つ
- 普段の配信基盤(Art19というサービス。音声ファイルを各ポッドキャストアプリに届けてくれる裏方さん)に、プレミアム限定エピソードを追加する
- 自分のポッドキャスト公式サイトで、通常回とプレミアム回を時系列でまとめて表示する
- Apple Podcastsなどのアプリでもプレミアムの回を購読できるようにする
- 解約したら即座に聴けなくする
- ちょっと噛み砕くと、「新聞の朝刊は誰でも読めるけど、有料版だけ届く号外がある。それをアプリでも自然に読めるようにしたい」というイメージ
- 「外部サービスに頼らず、フルスクラッチで作ってみたい」という個人開発欲もあり、自作することに
- Art19には「Alternate Feed(限定フィード)」という、決まった人にだけ配信を届ける機能があり、これを使う前提でスタート
フェーズ1:シンプルに2つのフィードをマージすればいいんじゃない?
- 通常フィードと限定フィードを両方取得して、時系列に混ぜて並べるだけで終わるかと思っていた
- 片方の取得が失敗しても、もう片方だけで表示を継続できる設計
- 壁1:Art19の埋め込みプレイヤーが、限定フィードのエピソードに対して404(ページが見つからないエラー)を返す
- 仕方なくブラウザ標準の音声再生機能(
<audio>タグ)で直接再生する方式に切り替え
- 仕方なくブラウザ標準の音声再生機能(
- 壁2:この方式で再生するには、配信情報(RSS)に書かれた音声ファイルの場所を直接読み取る必要がある
- サイトをビルドすると、その音声ファイルの場所がサイトの裏側のコード(JavaScript)にそのまま埋め込まれてしまい、ブラウザの開発者ツールを開けば誰でも丸見えの状態に
- つまりURLさえ知っていれば、お金を払っていない人でもエピソード一覧も音声も取り放題
- この方式はボツ
- 通常フィードと限定フィードを両方取得して、時系列に混ぜて並べるだけで終わるかと思っていた
フェーズ2:サーバーサイドに逃がせばURLを隠せる
- フィード取得の処理を裏側のサーバー(Firebase Cloud Functions)に移して、ブラウザ側にはフィードの場所を見せない構成に変更
- サーバー側で「この人はプレミアム会員か」を確認するチェックも実装
- 壁3:それでも結局、サーバーから返ってくる音声ファイルのURL自体が、コピーすればそのままアクセスできる直リンクだった
- 会員がそのURLを誰かに共有したら、共有された人はお金を払わずに聴けてしまう
- フィードの場所は隠せたけど、根本の問題は解決できず、またボツ
フェーズ3:ここで外部サービスを真剣に比較してみた
- ここは技術というより「どのサービスに乗るか」の値踏みの話
- Supercast:月額の固定費はなし、その代わり売上の5.5%+決済手数料をもらう仕組み。会員ごとに専用のフィードを発行してくれる。手数料率が少し惜しい
- Memberful:月額25ドル固定+決済手数料。会員ごとの専用フィードは魅力的だけど、個人運営には固定費が重い
- Patreon:手数料5〜12%。ただしポッドキャスト専門ではない
- 他番組の実例も調べた
- Rebuild.fm:専用サイト経由でダウンロードしてもらう、思い切ったスタイル
- backspace.fm:ブログサービスのGhostを使ってプレミアム配信をしっかり運用
- 結論(当時):月額25ドル固定はさすがに重い。じゃあ自分でSupercastみたいな仕組みを作ろうと決意。ここから沼にハマっていく
フェーズ4:Art19の認証機能を調べてみた → 断念
- Art19のサポートに相談したら、提案は2つ
- 方式1:合言葉方式(Access Token)。決まった合言葉をURLにくっつけてアクセスを許可する仕組み
- 問題:合言葉が全会員共通なので、1人からでも漏れたら全員アウトになる
- 合言葉を変えると、今度は真面目に登録している会員全員のフィードURLまで無効になる
- 方式2:身分証明書方式(JWT)。リクエストのたびに「これは私です」という証明書を一緒に送る仕組み
- 致命的な問題:ブラウザの音声再生機能やポッドキャストアプリは、リクエストに証明書のような追加情報を付け加えられない
- 結論:どちらの方式でも「ポッドキャストアプリから直接聴ける」という条件を満たせない
- 結局、間に立って通行手形をチェックする「プロキシ」というものを自分で用意するしかない、という話になる
フェーズ5:Cloudflare Workers で認証プロキシを自作(ここから中盤、エンジニア濃度が上がります)
- Cloudflare Workersというのは、ざっくり言うと「世界中のあちこちに置かれた、瞬時に起動する小さなプログラムの実行場所」。これを選んだ理由
- 起動の待ち時間がほぼゼロ(他の同種サービスは数秒〜数百ミリ秒かかることがある)
- 音声の巻き戻し・早送り(シーク)に必要な仕組みに対応できる
- 高速なデータ保存場所(KV)が内蔵されていて、アクセス許可の判定が一瞬で終わる
- 無料の利用枠が1日10万リクエストと十分
- 音声ファイル自体をこちら側にコピーしなかった理由
- 新しいエピソードが増えるたびに同期する手間や、削除・更新時のズレ、保管コストがかかる
- Art19から流れてくる音声をそのまま右から左に受け流す「取次係」だけで十分だった
- 全体の構成は2段構え
- Firebase:会員登録と決済管理(誰がプレミアム会員かのデータを保持)
- Cloudflare Workers + KV:リクエストが来るたびの「聴いていいか」の判定
- 期限付きの署名入りURLと、KVでの二重チェックでアクセスを制御
- 署名が有効でも、KV側の記録が消されていればアクセス不可。つまりURLが流出しても即座に無効化できる
- 決済サービス(Stripe)からの通知をきっかけに、会員データと許可情報の両方を更新する仕組み
- 音声の巻き戻し・早送りにも対応できるよう、リクエストの一部だけを転送する仕組みも実装
- 技術的には、これでやりたいことは全部実現できる設計になった
- Cloudflare Workersというのは、ざっくり言うと「世界中のあちこちに置かれた、瞬時に起動する小さなプログラムの実行場所」。これを選んだ理由
フェーズ6:ところが、この設計を全部捨てた
- 捨てた理由は技術的な問題じゃなく、維持していくコストとセキュリティ管理の負担
- 運用コストを試算してみた
- サーバー代の無料枠を超えた分:月5ドル〜
- データ保存の操作コスト:月0.5ドル〜
- 会員管理サーバーは無料枠内におさまる見込み
- 決済手数料:売上の3.6%
- 金額よりもずっと重かったのが「運用し続ける負担」
- 2つの異なるサービスを組み合わせているので、片方が壊れたときにもう片方とズレるリスク
- 署名の鍵を定期的に交換したり、データの有効期限を管理したりする手間
- 決済の通知が重複しても壊れないようにする設計や、失敗したときの再送設計
- そして何より、個人運営なのでこの全部の責任を自分ひとりで背負い続けることになる
- ここで気づいた。「自分が本来やりたいのは配信であって、インフラの運用ではない」
最終形:Substackに一本化
- 有料メディアプラットフォームのSubstackに乗り換えた条件
- 手数料:売上の10%(決済手数料込み)
- 月額固定費:0ドル
- 音声の保管・認証・会員管理・配信、全部込み
- 決め手になったのはSubstackの配信データの仕様
- 無料エピソードには音声ファイルの場所が書かれている
- 有料エピソードには、その場所の情報がそもそも書かれていない
- つまりサイト側の判定は「音声の場所が書かれていなければ有料」という、たった1行のシンプルな条件で済んでしまう
- 認証も、署名も、高速データ保存も、一切いらなくなった
- 最終的な構成は劇的にシンプルになった
- Art19とSubstack、両方の公開情報をサイト側でまとめて時系列表示するだけ
- 無料エピソード → その場でブラウザ再生
- 有料エピソード → Substackへのリンクにして、認証も再生もSubstack側にまるごと任せる
- 自作で残ったのは、ちょっとした通信の許可設定くらい
- 捨てたもの:自作の音声取次係、会員データの高速保存の仕組み、会員登録サーバー、そのデータベース、決済との直接連携
- 受け入れたトレードオフ
- 手数料が3.6%から10%に上がった(売上規模が大きくなったら見直せばいい、と割り切った)
- 再生画面を自由にデザインできない、アクセス解析を細かく見られない、アプリでの購読の自由度が下がる
- これらは「配信が軌道に乗るまでの保険料」として受け入れることにした
- 有料メディアプラットフォームのSubstackに乗り換えた条件
学びと結論
- 「2つの配信情報を混ぜるだけ」のつもりが、セキュリティを真面目に考え始めた瞬間に、芋づる式に問題が出てきた
- 一番の誤算は、ブラウザの音声再生機能が「これは私です」という証明書のようなものを一緒に送れない、というごく基本的な制約だった
- これに気づいた瞬間、ブラウザだけで完結する認証はすべて不可能になり、間に立つ仕組み(プロキシ)が必須になった
- 会員データを管理する場所と、アクセス許可を高速判定する場所の役割分担は、試行錯誤の末にたどり着いた設計で、これ自体は良い学びだった
- 設計を丸ごと捨てる判断について
- ここまで作ったのにもったいない、という気持ちはもちろんあった
- でも「自分が本当にやりたいのは配信であって、インフラの運用ではない」という本質に立ち返った
- ここまでの試行錯誤自体は「問題の解像度を上げるための学習投資」として意味があったと思っている
- 最終的な教訓は、認証・課金・配信のインフラは、個人でやるなら素直に外部サービスに任せたほうがいいということ
- 自分で作るのは学びにはなるけど、それをずっと維持し続けるのはまったく別の話
📎 https://zenn.dev/kkeeth/articles/podcast-premium-feed
エンディング
【ジングル】 さて、そろそろ今回もお時間です。 面白かったよーという方は、ぜひチャンネル登録もお願いします。話してほしいトピックや感想は、概要欄のフォームか 𝕏 で「WEB 小噺」でつぶやいてください。web はアルファベット、「小噺」は漢字でもひらがなでも大丈夫です! それでは、また雨宿りしに来てください。お相手は Keeth でした。さようなら! 【ジングル】
📚 参考情報
- 検討した構成の変遷
- フェーズ1〜2:フィードのマージ+サーバーサイド秘匿を試みるも、音声再生機能経由の直リンク露出が解決できず断念
- フェーズ3〜4:Supercast・Memberful・Patreonなど外部サービスを比較、Art19の認証機能(合言葉方式/身分証明書方式)も検証したが要件を満たせず
- フェーズ5:Cloudflare Workers + KVで署名付きURLと認可の2重チェックを行う認証プロキシを自作し、技術的には実現
- フェーズ6:メンテナンス・運用負担を理由に自作構成を全て破棄し、Substackへ一本化
- 最終的な学び
- ブラウザの音声再生機能は証明書のような追加情報を送れないため、ブラウザ経由の直接認証は不可能でプロキシが必須になる
- Substackは有料エピソードの配信情報に音声ファイルの場所を含めない仕様のため、「場所の情報がなければ有料」の1行で判定が完結する
- 個人運営では認証・課金・配信インフラは外部サービスに委譲する方が合理的
- 元記事
🎙️ 収録メモ
- トーン
- 冒頭:自虐混じり、経験談として。「今回はエンジニア寄りです」宣言も忘れずに
- そもそも/フェーズ3/フェーズ6/学びと結論:非エンジニアも置いてけぼりにしないよう、身近なたとえを交えてゆっくり
- フェーズ1・2・4・5:試行錯誤の臨場感、技術的に丁寧に。専門用語は都度かみ砕く
- フェーズ6以降:素直に、学びとして前向きに
- 強調ポイント
- 「ブラウザの音声再生機能は証明書を送れない」という最大の誤算
- 技術的には実現できたのに全部捨てた理由(運用負担)
- Substackの「有料エピソードには音声の場所情報がない」というシンプルな仕様
- アクセシビリティの目安
- 非エンジニアにも分かる想定の分量:そもそも/フェーズ3/フェーズ6/最終形の判定ロジック部分/学びと結論 → 全体の3〜4割程度
- エンジニア濃度が上がる部分:フェーズ1・2・4・5(用語には都度説明を入れている)
- 尺
- 合計:約15〜18分
- オープニング:1分
- 本編:13〜16分
- クロージング:1分
- タグ
ポッドキャスト #CloudflareWorkers #Firebase #Stripe #Substack #個人開発 #アーキテクチャ設計