2026-04-17 | Claude Code 作成 | 子安氏レビュー・加筆用の叩き台
Codex 5回目レビュー(14)で「v3 は 100店舗 SaaS として ship 不可、v4 を ADR/prototype から始めるべき」と結論された。
本書は CC(Claude Code)が作成した v4 設計の叩き台。子安氏にゼロから依頼するのではなく、叩き台をベースに具体的な議論を進めることで、子安氏の工数を圧縮しつつ方向性のズレを早く検出する目的で作成。
レビュー観点: 本書のすべての前提・判断・数値・代替案は子安氏の批判対象。最終的な ADR は子安氏加筆後に確定する。
| ラウンド | Codex指摘 | 結果 |
|---|---|---|
| 1回目 | scheduler/approval/cron/version 5件 | 🟢 修正済み |
| 2回目 | スケール + セキュリティ 11件 | 🟡 部分対応 |
| 3回目 | retry race / token / upload / audit / limit 5件 | 🟡 方向性OK・実装瑕疵 |
| 4回目 | audit再発 / migration / limit / upload 4件 | 🔴 未解決 |
| 5回目 | アーキテクチャ判定 | 🔴 No-ship for 100-store |
「v3 は、それらを提供しないプリミティブとフローから、atomicity / idempotency / authenticated authority / bounded memory / operational recovery を合成しようとしているため、修正するたびに同じ欠損 invariant が別サブシステムで再発する」
→ v3 のアーキテクチャ選択(Workers + KV + D1 中心)は 100店舗商用 SaaS の要件と構造的にミスマッチ。
Codex 5回目(5.1 セクション)で「root cause を invariants として reframe せよ」と指摘。以下はその 10項目。v4 のいかなる案もこの 10項目を満たすことを前提とする。
すべての副作用 API は認証された actor を持つ。匿名 bearer URL で Instagram 公開が起きてはならない。
v3 の失敗: 承認 URL token 単独で publish 起動可能だった(confirm_token を後付けしたが根本解決せず)
店舗(テナント)間のデータ・レート制限・監視イベントが絶対に混ざらない。クエリ・ストレージ・キャッシュのすべてで store_id 境界が機械的に enforce される。
予約投稿が Worker クラッシュ・タイムアウト・IG API 一時障害に遭っても、絶対に二重公開されない。成功・失敗・不明のどの状態からも reconciliation 可能。
v3 の失敗: ロック解放と外部 API 成功の間に window があり、重複投稿リスクが残存
画像処理は Worker isolate のメモリ上限(128MB)を絶対に超えない。streaming / object storage 経由のアップロードが標準フロー。
v3 の失敗: 20MB cap でも formData() parse が先に走りバッファリング
承認・公開・設定変更のすべてに tamper-evident な監査証跡が残る。並行書き込みで chain が fork しない。
v3 の失敗: 4ラウンド修正後も audit chain が並行書き込みで fork する
rate limit counter / audit chain head / publish state machine など「single writer が必要な箇所」で本物の atomic single writer が存在する。KV の read-then-write パターンで代用しない。
v3 の失敗: KV を atomic counter として使い race が毎ラウンド発生
投稿失敗 / Cron 遅延 / 認証攻撃 / audit 異常 がリアルタイムで検知され、担当者に通知される。Postmortem 可能なログが 90日保全される。
「Instagram には公開されたが DB には記録されていない」「DB には公開済みだが Instagram 側は未公開」などの partial failure を検出し、手動 or 自動で reconcile できる仕組み。RTO 1時間、RPO 5分。
Credential stuffing / 承認 URL 漏洩 / 画像 upload DoS / Cron retry storm などの攻撃に対する多層防御(WAF + Turnstile + atomic rate limit + quota)。
v4 リリース時に v2 → v4 の段階移行ができ、問題発生時は v2 へ戻せる。店舗単位で v2/v4 を切り替え可能。
Codex 5回目(5.3 セクション)で「store 数ではなく load envelope で判断せよ」と指摘。100店舗契約時の想定ピーク値を以下で定義する。
| 指標 | 想定値 | 備考 |
|---|---|---|
| 店舗数 | 100 | 契約上限 |
| 1店舗あたり月間投稿数 | 30〜90 | 平均 60 |
| 全体月間投稿数 | 3,000〜9,000 | 平均 6,000 |
| 全体日次投稿数 | 100〜300 | 平均 200 |
| 全体ログイン/日 | 500〜1,000 | 店舗あたり 5〜10 回 |
| Audit event/日 | 3,000〜6,000 | 投稿・ログイン・設定変更の合計 |
以下のピーク値を 各ゲートとして定義し、v4 アーキテクチャはこれを全て満たすことを検証する(実装前に prototype 負荷試験)。
| ゲートID | 指標 | 目標値 | v3 での達成度 |
|---|---|---|---|
| G1 | 同時アップロード(リクエスト並列度) | 50 req/分 | 🔴 128MB isolate 超過リスク |
| G2 | 同分予約投稿バースト | 50 件/分 | 🔴 LIMIT 5 polling で詰まる |
| G3 | Instagram API 同時コール | 100 req/5分 | 🟡 未計測 |
| G4 | ログイン攻撃時のPBKDF2並列度 | 10 fan-out 以下 | 🔴 並列 race で fan-out 無制限 |
| G5 | Audit 並行書き込み | 20 write/秒 で chain 整合維持 | 🔴 並行で fork する |
| G6 | retry storm 制御 | IG outage 1時間後に全件 5分以内で復旧 | 🔴 retry backoff 5分/15分固定 |
| G7 | recovery RTO | 1時間以内 | 🟡 未定義 |
| G8 | recovery RPO | 5分以内(データ喪失上限) | 🟡 未定義 |
| G9 | テナント分離テスト | 100%(他店舗データ参照不能) | 🟢 v2 時点で担保 |
| G10 | 本番マイグレーション所要 | 1店舗あたり 10分以内で v2→v4 切替可 | 🟡 未設計 |
┌─ Cloudflare Pages ─────────── フロントエンド(React+Vite) │ ├─ Cloudflare Workers ───────── fetch: auth / API routing / signed URL 発行 │ ├─ JWT verify / session │ └─ 軽量ロジックのみ(副作用は DO/Queue へ) │ ├─ Durable Objects(DO) │ ├─ PublishOrchestratorDO(per-post) │ │ - publish state machine(outbox pattern) │ │ - fencing token 保持 │ │ - Instagram API 照合 reconciliation │ ├─ RateLimitDO(per-IP) │ │ - atomic counter(PBKDF2 前の admission gate) │ │ - login / API / 承認の統合 abuse control │ └─ AuditAppenderDO(per-tenant) ← Codex指摘で singleton 回避 │ - hash chain head 保持 │ - atomic append │ ├─ Cloudflare Queues ────────── 予約投稿 / retry / 非同期処理 │ - Cron Trigger は enqueue のみ │ - Queue consumer → PublishOrchestratorDO │ ├─ Cloudflare R2 ────────────── 画像 object storage │ - Worker は signed URL 発行のみ │ - 画像 base64 化は行わない │ ├─ Cloudflare D1 ────────────── トランザクショナルメタデータ │ - stores / posts / approval_requests │ - atomic counter / chain head は DO へ │ ├─ Cloudflare WAF ───────────── /auth/login, /approval の rate limit 外層 ├─ Cloudflare Turnstile ────── 公開エンドポイントの bot/スパム対策 └─ Cloudflare Logpush → R2 ─── 90日監査ログ保全
| INV | 案A での実現手段 |
|---|---|
| INV-1 authenticated actor | 承認者は Cloudflare Access or 独自セッション(店舗アカウント+承認者サブアカウント) |
| INV-2 tenant isolation | D1 行レベル + DO は per-tenant namespace |
| INV-3 idempotent publish | PublishOrchestratorDO が outbox + fencing token + Graph API reconciliation |
| INV-4 bounded media | R2 direct upload(client → R2 signed URL)、Worker は base64 触れない |
| INV-5 durable audit | per-tenant AuditAppenderDO で atomic append |
| INV-6 single writer | RateLimitDO / AuditAppenderDO / PublishOrchestratorDO で atomic |
| INV-7 observable | Logpush + Sentry + Slack Webhook(06_v3運用監視設計 を流用) |
| INV-8 recovery | Queues DLQ + 手動 reconciliation script + IG API ID照合 |
| INV-9 abuse | WAF + Turnstile + RateLimitDO の多層 |
| INV-10 migration | v2/v4 両立運用可(Cloudflare だけで完結、DNS切替不要) |
┌─ Vercel or Cloudflare Pages ── フロントエンド(React+Vite) │ ├─ Fly.io or Railway container ─ アプリケーションサーバー(Node.js + Hono) │ or Supabase Edge Functions │ ├─ JWT verify / session / CSRF │ ├─ 業務ロジック(副作用含む) │ ├─ Postgres advisory lock(publish state machine の fencing) │ └─ Redis(rate limit counter / approval confirm token) │ ├─ PostgreSQL(Supabase or Neon or Fly Postgres) │ ├─ stores / posts / approval_requests(RLS でテナント分離) │ ├─ audit_logs(advisory lock で single writer 強制) │ └─ publish_outbox(トランザクショナル idempotency) │ ├─ Redis(Upstash or Fly Redis) │ - atomic counter(INCR/DECR でrate limit) │ - 短期キャッシュ / session blocklist │ ├─ Object storage(Supabase Storage or Cloudflare R2 or S3) │ - client → presigned URL で direct upload │ ├─ Job queue(pg-boss on Postgres or BullMQ on Redis) │ - 予約投稿 / retry / reconciliation │ ├─ Cloudflare WAF(DNS前段) └─ Sentry / Better Stack / Datadog for observability
| INV | 案B での実現手段 |
|---|---|
| INV-1 authenticated actor | Supabase Auth or Clerk or Auth.js(session-based、CSRF token標準装備) |
| INV-2 tenant isolation | Postgres RLS(Row Level Security)で機械的に enforce |
| INV-3 idempotent publish | publish_outbox テーブル + advisory lock + Graph API 照合 |
| INV-4 bounded media | presigned URL で client→object storage direct |
| INV-5 durable audit | Postgres トランザクション内で audit_logs append(hash chain) |
| INV-6 single writer | Postgres advisory lock / Redis atomic INCR |
| INV-7 observable | Sentry + Better Stack(構造化ログ・アラート) |
| INV-8 recovery | pg-boss retry + DLQ + Postgres point-in-time recovery |
| INV-9 abuse | Cloudflare WAF + Turnstile + Redis atomic counter |
| INV-10 migration | 店舗単位でDNS/アプリ切替(FF付き段階移行) |
| 項目 | 案A: Cloudflare-native | 案B: Postgres-based |
|---|---|---|
| コンピュート | Workers Paid $5 + DO requests ~$5 | Fly.io container 2×VM $30 or Supabase Pro $25 |
| データベース | D1 無料枠内 or $5 | Supabase Pro込み or Neon $19 |
| Queue | Queues ~$1 | pg-boss(DB同居・追加費用なし)or Upstash Redis $10 |
| オブジェクトストレージ | R2 $1〜2 | Supabase Storage込み or R2 $1〜2 or S3 $2 |
| Rate limit / counter | DO 内蔵(追加費用なし) | Redis Upstash $10 or Redis Fly $0 |
| WAF / Turnstile | Cloudflare WAF $20 + Turnstile 無料 | Cloudflare WAF $20 + Turnstile 無料 |
| Logpush | $5 | Better Stack $15 |
| Sentry | $26 | $26 |
| Claude API | $30〜50 | $30〜50 |
| 合計 | 約 $95〜120/月 | 約 $155〜200/月 |
売上 ¥3.5M/月(¥35,000 × 100店舗)に対し、案A 粗利 96%、案B 粗利 94%。コストは判断基準にならない(両案とも十分安い)。
| フェーズ | 案A | 案B |
|---|---|---|
| ADR 確定 + 小 prototype | 16〜24h | 16〜24h |
| コア実装(auth / publish / approval / upload / audit) | 80〜120h | 60〜100h(Postgresパターンの定番) |
| 運用監視(Sentry / Logpush / Slack) | 16〜24h | 16〜24h |
| 移行スクリプト(v2→v4) | 16〜24h | 24〜40h(Postgresデータ形式変換) |
| 負荷試験 + 本番 hardening | 24〜40h | 24〜40h |
| 合計 | 152〜232h(約3〜4ヶ月) | 140〜228h(約3〜4ヶ月) |
| 観点 | 案A | 案B | 勝者 |
|---|---|---|---|
| invariants 全 10項目を自然に満たす | ◯ | ◎ | B(Postgresの方が自然) |
| 100店舗規模での性能余裕 | ◎ | ◯ | A(エッジ実行) |
| プラットフォームロックイン回避 | ✕ | ◎ | B |
| 採用エンジニア確保 | △(DO経験者限定) | ◎(Postgres+Node.js標準) | B |
| 運用監視ツールの成熟度 | ◯ | ◎ | B |
| 月額コスト | ◎ | ◯ | A(差は小) |
| 既存 v2/v3 コードの流用 | ◎ | △(Node移植) | A |
| 子安氏の実装経験 | 不明 | 不明 | 要ヒアリング |
| プロダクション prior art | 少ない | 多い | B |
| デバッグ・観測の容易さ | △(DO中の状態追跡が困難) | ◎(SQL で可視化可) | B |
理由:
Week 1-2 ADR 確定(子安氏レビュー+加筆)+ 負荷envelope 再検証 Week 3 小prototype(両案で publish/audit の重要パスのみ実装)+ 負荷試験 Week 4 案確定 + 実装スコープ詳細化 Week 5-16 v4 実装(約3ヶ月) Week 17-18 本番負荷試験 + セキュリティレビュー Week 19-20 移行(店舗単位で段階カットオーバー、v2 は同時稼働) Week 21+ v4 のみで運用、v2 は retention 期間後に sunset
| 項目 | 運用方針 |
|---|---|
| 稼働店舗数 | 30店舗までに cap(契約条件に明記) |
| 機能追加 | 凍結(クリティカルバグ修正のみ) |
| v3 コード | freeze(リポジトリに参考資料として保存、ship しない) |
| 新規契約 | v4 リリースまで停止 or 30店舗 cap 契約のみ |
| リスク | 対応 |
|---|---|
| v2 → v4 データ移行ミス | 店舗単位段階移行、移行前後で投稿履歴を比較検証 |
| v4 で未知のバグ発生 | feature flag で v2 にロールバック可能に設計 |
| 移行中の店舗体験悪化 | 移行日を店舗と合意、深夜帯実施、48時間以内に完了保証 |
| 契約上の SLA 違反 | v2 期間中は「10〜30店舗の実績ベース」で SLA 設定、v4 以降に本契約SLA |
| リスク | 発生確率 | 影響 | 対応策 |
|---|---|---|---|
| ADR で案が決まらず時間浪費 | 中 | 高 | Week 2 末までに案決定、無理なら CC 推奨通り案B で進める |
| v4 実装が 3ヶ月を超える | 高 | 中 | スコープ削減(approval LINE通知・F-04 フィードバック学習は Phase 2 へ) |
| Instagram Graph API 仕様変更 | 低 | 高 | abstraction layer で隔離、v4 のコア設計は API 非依存に |
| 子安氏の工数確保困難 | 中 | 高 | 他エンジニアとの協業、CC が scoped 実装で支援 |
| 100店舗営業が想定より遅い | 中 | 低 | v4 完成時に 30店舗運用継続、段階的に拡大 |
| v2 が 30店舗 cap の間に事故 | 低 | 高 | 06運用監視設計 を v2 にも適用(8〜12h で実装可能) |
ここまでは叩き台。以下の項目について子安氏の判断・加筆をお願いしたい。
| 日付 | マイルストーン |
|---|---|
| Day 0(今日) | 本書送付 + 初回 MTG 日程調整 |
| Day 3〜7 | 子安氏 事前レビュー |
| Day 7〜10 | 初回 MTG(invariants / load envelope / 案選定) |
| Day 10〜17 | small prototype(両案 or 選定案のみ) |
| Day 17〜21 | ADR 確定 + Codex による ADR adversarial review |
| Day 21〜 | v4 実装開始 |