Skip to content

やまびこ (yamabiko) 仕様書 — Slack ハドル代替

ステータス: MVP 実装済み (初回デプロイ未実施)。 コードネーム「やまびこ」およびコマンド名 /yamabiko は仮称です。

Slack のハドル (Huddle) を Cloudflare Realtime (RealtimeKit) で代替する社内向け通話機能の正本仕様書です。apps/yamabiko-slack / apps/yamabiko-app / packages/yamabiko-core / packages/auth を改修する際は、先にこのドキュメントを読み、仕様変更を伴う場合は同じ PR で更新してください

背景と目的

  • Slack ハドルの代替となる「チャンネルから 1 クリックで始める気軽な通話」を自前のスタック (Cloudflare Workers + RealtimeKit) で提供する
  • ハドルとの差別化として、会話を揮発させない: 終了後に録音を AI が直接理解し、まとめをチャンネルのスレッドに残す
  • WebRTC の難所 (SFU、帯域適応、デバイス管理) は Cloudflare RealtimeKit (旧 Dyte) に委譲し、自前実装しない

決定事項 (機能定義の合意)

論点決定
MVP のメディア音声 + 画面共有 (ビデオはフェーズ 2)。RealtimeKit の preset で制御する
開始の入口スラッシュコマンド (/yamabiko → チャンネルに参加リンクを投稿)
参加者認証better-auth。このアプリ専用ではなく、他サービスでも共用する認証基盤 (packages/auth + 共有 D1 tied-auth) として新設
記録機能録音 → Cloudflare Workflow → Gemini の音声・動画理解で直接まとめ → スレッド投稿 を MVP に含める。書き起こしは介さない (transcription 機能は使わない)

アプリ全体像

名称種別役割
packages/yamabiko-core共有コア (framework 非依存 TS パッケージ)クリーンアーキテクチャ層 (domain / application / infrastructure / composition) を所有し exports で公開。両 Worker が依存する
packages/auth (@tied-workspace/auth)共有認証基盤better-auth の設定。drizzle スキーマと migrations は packages/d1 で統一管理し (@tied-workspace/d1/schema/tied-auth を import)、migrations は packages/d1/migrations/tied-auth/やまびこ専用ではなく、ワークスペースの他サービスも同じ DB (ユーザー / セッション) を共用する前提
yamabiko-slack (apps/yamabiko-slack)素の Cloudflare Worker (公開)/slack/commands (スラッシュコマンド) と /realtimekit/webhooks (参加/退出/終了) を受ける。署名検証 → 即 200 → ctx.waitUntil録音要約 Workflow yamabiko-postcall もこの Worker が持つ
yamabiko-web (apps/yamabiko-app)React Router v7 (framework mode / Cloudflare Workers)通話ルーム UI。loader が better-auth セッションを検証し、RealtimeKit の Add Participant で authToken を取得。クライアントは RealtimeKit React UI Kit (RtkMeeting) を描画する。公開 Worker だが better-auth で保護
Slack Appスラッシュコマンドを yamabiko-slack に POST する。Sign in with Slack (OIDC) のクレデンシャルも同じ App で発行する
RealtimeKitMeeting / Participant / preset / 録音 / webhook。API は Cloudflare API (/accounts/{account_id}/realtime/kit/{app_id}/…) を Cloudflare API トークン (Bearer) で呼ぶ (旧 Dyte の Org ID / API Key + Basic 認証は使わない)。App / preset / webhook は Cloudflare ダッシュボードで設定する

体験フロー

  1. メンバーがチャンネルで /yamabiko を実行する
  2. yamabiko-slack が署名検証 → 即時エフェメラル応答 →ctx.waitUntil で RealtimeKit Meeting を作成 (record_on_start: true) し、「🎙 ハドル開催中」メッセージ (参加ボタン付き Block Kit、初回投稿のみ @here 付き) をチャンネルへ投稿、チャンネル名へ開催中プレフィックス 🎙- を付け (best-effort、F-09)、状態を KV へ保存する。開催中に再実行した場合は既存リンクをエフェメラルで返す (冪等)
  3. 参加ボタン → yamabiko-web/room/:meetingId。未サインインなら /sign-in (Sign in with Slack / OIDC) を経て、loader が Add Participant → authToken でブラウザの通話 UI (音声 + 画面共有) に入る
  4. RealtimeKit webhook (参加/退出) を yamabiko-slack が受け、Slack メッセージの参加者リストを chat.update する
  5. 全員退出または meeting 終了でハドル終了。メッセージを「🎙 ハドル終了 (N分)」へ更新し、チャンネル名を元へ戻して (best-effort)、Workflow yamabiko-postcall を起動する
  6. Workflow が録音のアップロード完了を待ち → R2 へ退避 → Gemini が録音を直接聞いて日本語のまとめ (要約 / 決定事項 / TODO) を生成 → ハドルメッセージのスレッドへ投稿する

機能一覧 (MVP)

ID機能実装
F-01ハドル開始 (1 チャンネル 1 ハドル、冪等)。webhook を取りこぼして active のまま残ったハドルは、開始 10 分経過後の再実行時に RealtimeKit の active session を確認して自己修復 (終了処理してから新規作成) するstartHuddle (application/usecases/start-huddle.ts)
F-02参加リンク投稿 / 更新 (Block Kit)renderHuddleMessage (application/huddle-message.ts) + SlackApiNotifier
F-03通話ルーム UI (音声 + 画面共有)apps/yamabiko-app /room/:meetingId + RealtimeKit React UI Kit
F-04サインイン (better-auth / Sign in with Slack)packages/auth + /sign-in /api/auth/*
F-05参加者同期 (webhook → メッセージ更新)handleMeetingEvent (application/usecases/handle-meeting-event.ts)
F-06終了判定 (全員退出 or meeting.ended)。一度も参加者が居なかったハドルは録音が無いため postcall を起動しないendHuddle (application/usecases/end-huddle.ts、冪等)
F-07録音の回収と R2 退避Workflow yamabiko-postcall + R2RecordingStore
F-08AI まとめのスレッド投稿 (Gemini 音声・動画理解)。15MB 以下は inline、超は Gemini Files API (最大 2GB / 48h 自動失効 / 64MB 超はストリーミングアップロード)。動画 45 分超は低解像度 (MEDIA_RESOLUTION_LOW) で送り、コンテキスト上限の目安 (動画 170 分 / 音声 9 時間) を超える録音はスキップsummarizePostcall + GeminiMediaSummarizer (AI Gateway 経由)
F-09ハドル開始の可視化: 開始時にチャンネル名へ 🎙- プレフィックスを付け、終了時に元へ戻す。conversations.rename はチャンネル作成者 / Workspace Admin / Channel Manager しか実行できず、bot が作成していないチャンネルでは not_authorized になりうるため best-effort (失敗してもハドルは続行し、channelRenamed: false で復元もスキップ)。装飾済みの名前には二重に付けず、復元失敗で残ったプレフィックスは次のハドル終了時に自己修復される。開催メッセージの初回投稿には @here を含める (chat.update には含めず再通知を避ける)decorateChannelName (application/channel-name.ts) + startHuddle / endHuddle

非スコープ (フェーズ 2 以降)

  • ビデオ (カメラ映像)
  • 録音ファイルの共有 UI (録音は R2 に残るが配信導線は無い)
  • Web UI からのルーム作成・常設ルーム
  • 社外ゲストの参加
  • DM / グループ DM でのハドル
  • Slack ステータス連動・通話中リアクション

データモデルとストレージ

  • KV HUDDLE_KV (両 Worker で同一 namespace を共有)
    • huddle:channel:<channelId> — チャンネルの現在のハドル (JSON)
    • huddle:meeting:<meetingId> — 同じ JSON の meeting 引き (webhook / ルーム UI 用)。チャンネルで新ハドルが始まっても旧 meeting キーは残り、postcall が参照できる
    • JSON には終了時のチャンネル名復元用に channelName (装飾前の名前) と channelRenamed (開始時に rename できたか) を含む (F-09)
    • active は TTL なし、ended は TTL 7 日で自動消滅
  • R2 yamabiko-recordingsrecordings/<meetingId>/<recordingId> に録音を退避 (ダウンロード URL が期限付きのため)
  • D1 tied-auth — better-auth の user / session / account / verification。共有 DB として複数 Worker からバインドする。drizzle 定義は packages/d1/schema/tied-auth.ts (@tied-workspace/auth が import する)、migrations は packages/d1/migrations/tied-auth/ が正本 (D1 スキーマ統一管理の規約)。適用は bunx wrangler d1 migrations apply tied-auth --remote -c packages/d1/wrangler.toml

認証基盤 (better-auth) の位置づけ

リポジトリ規約からの意図的な逸脱

本リポジトリの規約は「アクセス制御は Cloudflare Access (Zero Trust) に任せ、アプリに認証ロジックを書かない」です。やまびこの通話ルームはこの規約から意図的に逸脱し、better-auth によるアプリ層認証を採用します。理由:

  1. Slack のメッセージから誰でも開ける動線であり、将来のゲスト参加 (フェーズ 2) も視野に入れると、Access の組織メンバー前提モデルと相性が悪い
  2. better-auth + 共有 D1 をワークスペース共通の認証基盤として整備する方針が決まっており、本アプリがその最初の利用者となる

ただし認証ロジックは packages/auth のみに置き、アプリ個別には書かない (CLAUDE.md の Conventions にも例外として明記済み)。

  • IdP は Sign in with Slack (OpenID Connect)。better-auth に組み込みプロバイダーが無いため genericOAuth プラグイン + Slack の OIDC discovery (https://slack.com/.well-known/openid-configuration) で構成する
  • Slack OIDC の sub クレームは Slack ユーザー ID のため、account.accountId に Slack ユーザー ID が入る。ルーム参加時はこれを RealtimeKit の custom_participant_id に渡し、webhook の参加者と Slack ユーザーを突合する
  • OIDC コールバック URL: ${BETTER_AUTH_URL}/api/auth/oauth2/callback/slack
  • 他サービスから使う場合: 同じ D1 をバインドし createAuth を同じ secret / 各アプリの baseURL で mount する。サブドメイン間 SSO (cookie ドメイン共有) は必要になった時点で設計する

設計の不変条件 (Invariants)

  1. 通話メディアは RealtimeKit に委譲する: SFU・シグナリング・帯域制御を自前実装しない。クライアントは Core SDK + UI Kit を使う
  2. Slack の 3 秒 ack を守る: スラッシュコマンドは署名検証 → 即時応答 → ctx.waitUntil (suteki-bot / noisy-crow-events と同じパターン)
  3. 公開 Worker の真正性検証: Slack 起点は Slack 署名 (HMAC-SHA256、5 分リプレイ窓)。RealtimeKit 起点の webhook は RealtimeKit の公開鍵による RSA-SHA256 署名検証 (公開鍵は /.well-known/webhooks.json から取得・キャッシュ)。webhook にシークレットは存在しない
  4. webhook 処理は冪等にする: 参加は participant id で重複排除、終了済みハドルへのイベントは無視 (postcall の二重起動を防ぐ)
  5. 録音要約を HTTP リクエスト内で実行しない: 録音の完成待ち + ダウンロード + AI 呼び出しは長時間処理のため、必ず Workflow yamabiko-postcall で実行する。Workflow が失敗した場合はスレッドへ失敗の痕跡を残してから fail させる (黙って消えない)
  6. AI 呼び出しは AI Gateway 経由 (リポジトリ規約を踏襲)。録音は書き起こしを介さず Gemini の音声・動画理解に直接渡す
  7. 認証ロジックは packages/auth のみに置く: 共有認証基盤の変更は利用する全サービスへの影響を確認してから行う
  8. クリーンアーキテクチャを崩さない: domainapplicationinfrastructure の依存方向を保つ。共有コアは framework 非依存
  9. TypeScript の相対 import に拡張子を付けない (moduleResolution: "bundler" 統一)

運用 / デプロイ

初回セットアップ

  1. Cloudflare リソース作成

    KV yamabiko-huddle (2d5bf5b9f2244cf98d8702340afd4b70) と D1 tied-auth (68870711-c65a-46a4-a82e-0737c91a565b) は作成済みで、各 wrangler.toml に id 設定済み。残りは:

    bash
    bunx wrangler r2 bucket create yamabiko-recordings
    bunx wrangler d1 migrations apply tied-auth --remote -c packages/d1/wrangler.toml

    AI Gateway yamabiko-ai-gateway をダッシュボードで作成する。Authenticated Gateway を有効化し、Google API キーはゲートウェイの BYOK (stored keys) に保存する。Worker からの Gemini 認証は AI_GATEWAY_TOKEN のみで行い、GOOGLE_API_KEY は Worker に持たない

  2. RealtimeKit: App は作成済み (App ID db6fc7e4-d7c1-4037-8050-6c6c57a4a76e、両 wrangler.toml の REALTIMEKIT_APP_ID に設定済み)。残りは: RealtimeKit 権限を持つ Cloudflare API トークンを作成REALTIMEKIT_API_TOKEN (secret) に設定。preset (既定名 group_call_participant音声 + 画面共有のみ・ビデオ無効) を作成。webhook を https://<yamabiko-slack のドメイン>/realtimekit/webhooks に登録する (登録は name / url / events のみでシークレットは無い。真正性は公開鍵署名で検証する)

  3. Slack App: 専用 App を新設 (suteki-bot とは分離)。スラッシュコマンド /yamabikohttps://<yamabiko-slack のドメイン>/slack/commands。Bot scope: commands chat:write channels:manage (公開チャンネルの rename、F-09) groups:write (プライベートチャンネルの rename)。OIDC (Sign in with Slack) の Redirect URL: https://yamabiko.tied-workspace.com/api/auth/oauth2/callback/slack (BETTER_AUTH_URL と一致させる)。使うチャンネルにはボットを /invite する

  4. Secrets: 各 wrangler.toml 末尾のコメントに列挙したものを wrangler secret put で設定する

デプロイ

bash
bun run --filter @tied-workspace/yamabiko-slack deploy   # Slack 受け + postcall Workflow
bun run --filter @tied-workspace/yamabiko-app deploy     # ルーム UI (yamabiko-web)

# ビルド検証のみ (デプロイしない。Cloudflare クレデンシャル不要)
bun run --filter @tied-workspace/yamabiko-slack build    # wrangler deploy --dry-run
bun run --filter @tied-workspace/yamabiko-app build      # react-router build

CI (GitHub Actions)

.github/workflows/deploy-yamabiko.yml (noisy-crow と同型):

  • PR: 関連パス (apps/yamabiko-* / packages/yamabiko-core / packages/auth) の変更で check ジョブが走り、typecheck + bun test + RR7 ビルドを検証する
  • main へ push: production ジョブが両 Worker を自動デプロイする (CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID は既存の repo secrets)
  • 横断 CI (.github/workflows/ci.yml): path フィルタ無しで全 PR に走り、bun run --filter '*' build (yamabiko-slack は dry-run) と bun run --filter '*' test を実行する

テスト / 型チェック

bash
bun run --filter @tied-workspace/yamabiko-core test
bun run --filter @tied-workspace/yamabiko-slack test
bun run --filter @tied-workspace/yamabiko-core typecheck
bun run --filter @tied-workspace/yamabiko-app typecheck

実装時に検証が必要な点 (TODO)

初回デプロイ時に実 API で確認し、確定したらコードの TODO コメントと本表を更新すること。

項目現在の実装 (想定)確認方法
RealtimeKit webhook の署名ヘッダー名RealtimeKit の公開鍵による RSA-SHA256 (base64) を複数ヘッダー候補 (rtk-signature / dyte-signature 等) から受ける (verify-realtimekit-signature.ts)。公開鍵の所在とレスポンス形は実環境で確認済み: https://api.realtime.cloudflare.com/.well-known/webhooks.json{success, data: {publicKey: "<SPKI PEM>"}} を返す。REALTIMEKIT_WEBHOOK_PUBLIC_KEY (PEM) で上書き可ヘッダー名のみ実配送ログで確定して絞り込む
webhook イベント名 / payload 形meeting.participantJoined / meeting.participantLeft / meeting.ended、participant フィールドは複数候補から拾う (webhook-event.ts)テスト配送のログ
recordings API のレスポンス形GET /accounts/{account_id}/realtime/kit/{app_id}/recordings?meeting_id=data[].status === "UPLOADED"audio_download_url 優先。Add Participant の token フィールド名も揺れを許容実録音で確認
@cloudflare/realtimekit-* npm パッケージのバージョン^1.0.0 を仮置きbun install で解決確認
大きい録音の Files API 経由要約15MB 超は Files API へ resumable アップロード (GeminiMediaSummarizer)。要検証: AI Gateway 経由の /upload/v1beta/files パスで BYOK のキー注入が効くか、64MB 超のストリーム body アップロードが通るか15MB 超の実録音で確認
RealtimeKit GA 後の料金ベータ中は無料GA アナウンス時に再評価
チャンネル rename の権限 (F-09)conversations.renameチャンネル作成者 / Workspace Admin / Channel Manager のみ実行可能で、bot が作成していないチャンネルでは not_authorized を想定。失敗時は装飾なしで続行する best-effort 実装実ワークスペースの対象チャンネルで bot トークンの rename が通るか確認。通らない場合の運用 (対象チャンネルを bot に作らせる / 機能を外す) を決める。絵文字入りチャンネル名 (🎙-) が API で受理されることも併せて確認

参考リンク