メインコンテンツまでスキップ

技術スタック

選定結果サマリ

レイヤー選定技術
FrontendNext.js (React) → Vercel
BackendHono (TypeScript) → Fly.io
DatabaseTurso (SQLite)
Search EngineMeilisearch (Hetzner US セルフホスト)
AuthBetter Auth (Google)
PaymentStripe
Subtitleyoutube-transcript-plus
Batch JobGitHub Actions (Scheduled Workflow)

Frontend: Next.js (React)

選定理由

  • SSR/SSG対応でSEO・OGPの要件を満たせる
  • i18n(7言語対応)のエコシステムが充実(next-intl等)
  • URLクエリパラメータによる状態管理と相性が良い
  • Dark/Lightモード対応のライブラリが豊富
  • Vercelにゼロコンフィグでデプロイ可能
  • エコシステム・情報量が最大

比較した候補

候補不採用理由
Nuxt (Vue)Next.jsと同等の機能を持つが、エコシステムの規模・情報量で劣る。Vue自体の好みがなければ選ぶ理由が薄い
SvelteKit軽量・高速だがエコシステムが小さく、i18nやUIコンポーネントの選択肢が限られる

Backend: Hono (TypeScript)

選定理由

  • フロントエンド (Next.js) と言語を統一でき、型定義の共有が可能
  • 軽量・高速なAPIフレームワーク
  • Fly.ioへのデプロイが容易

比較した候補

候補不採用理由
FastAPI (Python)youtube-transcript-api (Python) や pykakasi (Python) とは相性が良いが、フロントと言語が分かれる。字幕取得は youtube-transcript-plus (TS) で代替可能、日本語表記ゆれはMeilisearchで対応することで、Pythonを選ぶ必要がなくなった
Django (Python)フルスタックで機能過多。APIサーバーとしてはオーバースペック
Express (Node.js)動作するが、Honoの方が軽量・型安全・モダン

補足: Cloudflare Workers への移行

経緯

  1. 初期検討時: kuromoji の辞書ファイル(約20MB)が Workers のサイズ制限に収まらず断念
  2. Meilisearch 採用後: kuromoji が不要になり Workers 対応は技術的に可能になった
  3. Meilisearch を Hetzner US にセルフホストする決定後: Workers でも Fly.io でも Meilisearch との通信はネットワーク跨ぎで同条件
  4. Fly.io を選定 → CI/CD 構築時に Fly.io 256MB VM で OOM クラッシュが発生し、Workers に移行を決定

それでも Fly.io を選ぶ理由(2026-04 時点で解消済み)

1. Wall time 制限

Workers にはリクエストあたりの実行時間(Wall time)に制限がある。

2025 年以降、Paid プランで Wall time は無制限に変更された。 CPU time のみ制限あり(デフォルト 30 秒、最大 5 分に設定可能)。字幕取得・SSE 接続の大半は I/O 待ちのため CPU time をほぼ消費しない。

2. コスト

Fly.io のコスト比較は Unbound プラン前提だったが、現在 Unbound は廃止され Paid プラン($5/月 + 従量課金)に統合。50 万 PV 以上で Workers の方がコスト優位:

月間 PVFly.io 512MB (2台)Workers Paid
10 万~$1-2$5.00
50 万~$6.6$5.00
100 万~$13.25$5.00
860 万(競合 Filmot 級)~$36$11.80

3. 運用のシンプルさ

Wall time 無制限により Workers 単体で全処理が完結する。Docker ビルド不要、スケーリング自動、OOM 管理不要で Fly.io より運用が簡素。

Workers 移行で使用する互換性フラグ

nodejs_compat + nodejs_compat_populate_process_env により、既存の process.env ベースのコードを変更せずに Workers 上で動作させている。


Database: Turso (SQLite)

選定理由

  • 軽量・シンプルなSQLiteベースのマネージドDB
  • Better Authがネイティブでサポート
  • 無料枠が充実(10億行読み取り/月、2,500万行書き込み/月、9GBストレージ)
  • Meilisearchが検索を担当するため、DBには複雑なクエリが不要 → SQLiteで十分
  • 次の有料プラン(Developer)も$4.99/月と安い

比較した候補

候補不採用理由
Supabase PostgreSQLAuth統合が強みだったが、Better Authを採用したため利点が薄い。検索はMeilisearchが担当するため、PostgreSQLの全文検索機能も不要。単純なCRUD用途にはオーバースペック
PlanetScale (MySQL)Tursoと同等の用途に使えるが、SQLiteの方が軽量でBetter Authとの統合がスムーズ
D1 (Cloudflare)SQLite互換だが、バックエンドがFly.ioなら利点が薄い

用途

  • ユーザー情報(プラン、クレジット残高)
  • 動画メタデータ(タイトル、チャンネル、投稿日等)
  • お気に入りチャンネル
  • 検索ログ
  • アクセス権管理(user_video_access

※ 字幕テキストは Meilisearch に保存(検索用 + 元データ内部保持)。Turso には字幕テキスト本体を持たない。Cloudflare R2 は MVP では不要(詳細は 検索インフラ調査 を参照)。


Search Engine: Meilisearch (Hetzner US セルフホスト)

選定理由

  • 多言語トークナイズ内蔵(日本語・中国語・韓国語を含む7言語に対応)
  • タイポ許容検索(「yotube」→「youtube」)
  • 検索ワードのハイライトをAPIが自動で返す(機能仕様の「検索ワードをBoldで強調」を実現)
  • 同義語辞書で日本語の表記ゆれに対応可能(「最高」=「さいこう」等)
  • 50ms以下の高速レスポンス
  • セルフホストなら無料
  • Dockerイメージ1つで起動でき、運用が容易

Subseekで具体的に使える機能

仕様の要件Meilisearchの機能
検索ワードをBoldで強調ハイライト機能(_formattedフィールドで自動返却)
関連度順・新着順の並び替えソート機能(sort: ['publishedAt:desc']
ページネーションoffset/limit パラメータ
チャンネル絞り込みフィルタ機能(filter: 'channelId = "UCxxx"'
検索対象の切り替え(タイトル/説明文/字幕)検索対象フィールド指定
言語絞り込みフィルタ機能(filter: 'language = "ja"'
チャンネルごとの件数表示ファセット機能(facets: ['channelId']

データ管理

MeilisearchはRDBではなく、JSONドキュメントを投げるだけで全文インデックスが自動構築される。スキーマ定義は不要。データ登録時に全テキストのインデックスが自動で作られるため、検索されたことがない単語でも即座にヒットする。

ホスティングコスト見積もり(Hetzner US セルフホスト)

セグメント単位(1動画500セグメント)でドキュメントを投入する前提。詳細は 検索インフラ調査 を参照。ホスティング比較は Meilisearch ホスティング比較 を参照。

フェーズ動画数セグメント数Hetzner US プラン月額
初期(〜1,000動画)1,00050万CPX11: 2vCPU/2GB≈750円
中期(〜1万動画)1万500万CPX21: 3vCPU/4GB≈1,500円
成長期(〜10万動画)10万5,000万CPX31: 4vCPU/8GB≈3,000〜3,750円
大規模(〜100万動画)100万5億CPX41: 8vCPU/16GB + 590GB ボリューム≈12,000〜15,000円

比較した候補

候補不採用理由
PostgreSQL全文検索 (tsvector)日本語のトークナイズに標準では対応しない。pg_bigm等の拡張が必要。ハイライト機能も自前実装が必要
Typesenseインデックスをメモリに保持する設計のため、5億セグメント規模ではメモリコストが高騰(100万動画で ≈$3,000/月)
Algoliaレコード数課金($0.40/1K件)のため、5,000万セグメントで ≈$20,000/月。論外
Elasticsearch / OpenSearch高機能だが運用が重い(クラスタ管理、シャード設計等)。subseek の規模にはオーバースペック
Meilisearch Cloud (Resource-based)マネージドで運用不要だが、Hetzner US の2〜7倍のコスト。大規模時の移行先として検討
Meilisearch Fly.io セルフホスト小規模では安いが、100万動画で Fly.io の 500GB ボリューム上限によりシャーディングが必要
Cloudflare Vectorize + Turso FTS5セマンティック検索 + 全文検索を分離する構成。コストは安いが、スコア統合の自前実装・Vectorize の 1,000万ベクトル/インデックス上限・短文での精度未知数がネック

Storage: Cloudflare R2(BAN 対策バックアップ)

Cloudflare R2 を Meilisearch ダンプのバックアップ保存先として使用する。Hetzner のアカウント BAN リスクに備え、毎日ダンプを R2 に自動保存する。

構成(現行)

Meilisearch on Hetzner US: 字幕テキスト(検索用インデックス + 元データ内部保持)
Cloudflare R2: Meilisearch ダンプ(BAN 対策バックアップ)
Turso: ユーザー情報・動画メタデータのみ(字幕テキストは持たない)

バックアップの目的

  • Hetzner の予告なしアカウント停止に備える(データにアクセスできなくなるリスク)
  • BAN 時に R2 のダンプから Meilisearch Cloud に1〜2日で移行可能
  • 実装は v0.3 で対応(#77)
  • 詳細は 検索インフラ調査 を参照

Subtitle: youtube-transcript-plus

選定理由

  • TypeScript製でHonoと言語統一
  • 多言語対応・フォーマット変換(SRT/VTT/テキスト)・メタデータ取得に対応
  • キャッシュ機構・指数バックオフリトライが内蔵されており、プロダクション利用に適する
  • youtube-transcript(Node.js版の基本ライブラリ)のフォーク・強化版

比較した候補

候補不採用理由
youtube-transcript-api (Python)最も実績あり(★7.2K、398コミット)だが、Python製のためTypeScriptバックエンドとは言語が合わない
youtube-transcript (Node.js)TypeScript製だが機能が薄い(★546、24コミット)。多言語対応・リトライ・キャッシュなし。メンテナンスも不安(21 open issues放置気味)

Auth: Better Auth

選定理由

  • TypeScript製のフレームワーク非依存認証ライブラリ(★27.5K、活発に開発中)
  • GoogleなどのOAuthプロバイダーをサポート
  • Turso (SQLite) をネイティブサポート
  • Stripe連携プラグインが組み込み → 課金との統合がスムーズ
  • 2FA・マルチテナント等の高度な機能もプラグインで拡張可能

比較した候補

候補不採用理由
Supabase AuthSupabase PostgreSQLとの統合が強みだったが、DBをTursoに変更したため利点がなくなった
Clerk高機能だが有料($25/月〜)。Better Authならセルフホストで無料
NextAuth.js (Auth.js)自前管理が必要で設定が煩雑。Better Authの方が機能が充実しており、フレームワーク非依存

Payment: Stripe

選定理由

  • 業界標準の決済プラットフォーム
  • Checkout・Billing・Webhookが充実
  • 日本円対応
  • ドキュメント・SDKの品質が高い
  • Better AuthのStripe連携プラグインで統合が容易

※ 他の決済サービスは検討していない。Stripeが事実上の標準。

Stripe Projects について(2025年発表・Developer Preview)

Stripe Projects は開発インフラのプロビジョニング・管理CLIであり、subseek のエンドユーザー向け課金(Stripe Billing / Subscriptions)とは無関係。導入しても現在のアプリケーション構成は変わらない。

観点Stripe Projects がやることsubseek への影響
認証Clerk 等の外部サービスをプロビジョニングなし(Better Auth を使用)
DBTurso / Neon 等をプロビジョニングなし(Turso を使用)
課金開発者自身の SaaS 利用料を一括管理なし(ユーザー課金は Stripe Billing で別管理)
ホスティングVercel / Fly.io 等をプロビジョニングなし(デプロイ先は変わらない)

参考: https://projects.dev/ / https://docs.stripe.com/projects


Deploy

Frontend: Vercel

  • Next.jsの開発元が運営。ゼロコンフィグでデプロイ可能
  • 無料枠で初期フェーズは十分

Backend: Fly.io

  • Dockerコンテナベースで Hono をデプロイ
  • 実行時間の制限なし(字幕取得パイプライン、SSE 等の長時間処理に対応)
  • Vercel (Frontend) ↔ Fly.io (API) ↔ Turso (DB) / Hetzner (Meilisearch) で責務が分離
  • 最安プラン $2/月〜

Meilisearch: Hetzner US (Ashburn) セルフホスト

  • Docker でセルフホスト。詳細は Meilisearch ホスティング比較 を参照
  • Fly.io の Meilisearch セルフホストと比較して2〜7倍安い
  • CPX11(≈750円/月)〜 で運用可能

比較した候補

候補不採用理由
Cloudflare WorkersHono と相性が良いが、Wall time 制限(30秒)があり字幕取得パイプラインが動かない。Unbound プランなら可能だが Fly.io より高コスト。詳細は「補足: Cloudflare Workers について」を参照
RailwayFly.ioと同等だが、無料枠がない
Cloud Run (GCP)スケーラブルだが、初期フェーズにはオーバースペック

Batch Job: GitHub Actions (Scheduled Workflow)

選定理由

  • 月1回の軽量バッチ(3つのSQL実行)に対して、専用インフラは不要
  • GitHub Actions の schedule トリガー(cron式)で十分
  • Turso の HTTP API により、GitHub Actions Runner から直接DB操作が可能
  • 実行ログ・失敗通知が GitHub に統合されており、別途監視基盤が不要
  • 実行時刻の遅延(数分〜数十分)は月次バッチでは許容範囲

実行するジョブ(月初1日)

#処理概要
1期限切れアクセス権削除user_video_accessexpires_at < NOW() のレコードを物理削除
2孤立匿名ユーザー削除accounts にレコードがなく、user_video_access にも関連がない users を削除
3クレジット月次リセットbilling_accountscredit_balance をプラン上限値で上書き
4削除済み動画の物理削除videossubtitle_status = 'deleted' のレコードと対応する R2 データを削除

※ 処理順序は 1 → 2 → 3 → 4(1 の結果が 2 の判定に影響するため。4 は独立だが最後に実行) ※ #2 は機能的には不要(データ衛生目的のみ)。匿名ユーザーは Cookie 失効(60日)後に再訪問すると新規ユーザーとして扱われるため、孤立レコードが残っていてもユーザー体験に影響しない。初期フェーズではスキップしてもよい。

比較した候補

候補不採用理由
Fly.io Cron Managerジョブごとに一時 Machine を起動する本格的なソリューション。月1回の軽量バッチには過剰
Fly.io Supercronicバックエンドコンテナ内で crontab を実行。再起動でスケジュールがリセットされるリスクがあり、コンテナに cron の責務が混在する
Cloudflare Workers Cron Triggers高信頼だが、現在 Workers を使用していない(Fly.io に統一済み)。月1回のバッチのために新たなインフラを追加する理由がない

注意事項

  • TURSO_DATABASE_URLTURSO_AUTH_TOKEN を GitHub Secrets に登録する
  • ジョブはべき等に実装する(重複実行しても安全な設計にする)