v4 Decision ADR v2 — Codex 7回目指摘対応版

2026-04-17 | Claude Code | 17 の改訂版(6項目 fix)+ Codex 8回目レビュー予定

📌 本書の位置づけ

17(Decision ADR v1)は Codex 7回目レビュー(18)で needs-major-revision 判定。子安氏送付前にさらに 1ラウンド改訂した本書 v2 が最終候補。 Codex 7回目指摘 6件(critical 1 / high 4 / medium 1)を全て織り込み済み。

変更点サマリ: ①γ専用 Secrets 設計追加(pgsodium 参照削除) ②TCO を 30/100/300店舗 scenario 化 ③Operating Model 二択明示(24/7 採用) ④スコアリング rubric 導入 + unknown マーク ⑤Evidence を failure matrix 化 ⑥Invariant に INV-16〜20 追加(multi-vendor SLA / compliance)

📑 目次

1. 実現したいこと(要件)

1.1 ビジネス要件

項目内容
事業モデル飲食店向け Instagram 自動投稿 SaaS
料金通常 ¥20,000/月 / 上位 ¥35,000/月
短期10→30 店舗
中期100 店舗
長期300 店舗(水平拡大上限想定)
100店舗時売上¥3.5M/月(想定 80% が上位プラン)
商用約束予約投稿の確実実行・投稿履歴保全・承認フロー・監査証跡

1.2 機能要件(17と同一、再掲)

ID機能v4 優先度
F-A画像アップロード + AIキャプション生成(Claude)MVP
F-B即時投稿(IG Graph API)MVP
F-01予約投稿MVP
F-02投稿履歴画面MVP
F-03店舗設定 GUIMVP
F-06IG トークン自動更新MVP
F-07投稿承認フローMVP
F-08投稿メソッドモードMVP
F-03b店舗設定 GUI 拡充Phase 2
F-09LINE 通知Phase 2
F-04フィードバック学習Phase 2
F-05助っ人 API 連携Phase 3

1.3 運用要件

項目内容
運用チーム子安氏1名 + CC 支援 + 高木氏(ビジネス窓口)
SLO予約投稿成功率 99.5% / 投稿API p95 < 60秒 / 月間 downtime < 2時間
RTO / RPORTO 1時間 / RPO 5分
On-call24/7 escalation(§12 で後述)

2. Invariants(v2: 20項目に拡張)

v1 からの変更: Codex 7回目指摘 #6 対応で INV-16〜20 を追加。v1 の 15項目は全て維持。全 20項目 MUST(launch-blocking)。

2.1 v1 から継承(15項目)

IDInvariantWhy
INV-1Authenticated Actor Model匿名 bearer URL で IG 公開不可
INV-2Tenant Isolation(行レベル)100店舗間のデータ混在禁止
INV-3Idempotent Instagram PublishWorker crash 等で重複公開しない
INV-4Bounded Media PathWorker メモリ上限超過しない
INV-5Durable Audit Trailtamper-evident / 並行 fork しない
INV-6Single Writer Where Requiredrate limit / audit / publish state は atomic
INV-7Observable Operationsリアルタイム検知 / 90日ログ保全
INV-8Recovery from Partial FailureRTO 1h / RPO 5min
INV-9Abuse ControlsCredential stuffing / URL 漏洩対策
INV-10Migration Rollbackstore 単位 rollback 可
INV-11Schema Evolutionzero-downtime DB migration
INV-12Backup / Restore DrillPITR + 月次 restore 演習
INV-13Secrets LifecycleJWT/暗号鍵/IG token の rotation / revocation / audit
INV-14Retention / Deletion(GDPR)解約時のデータ削除・削除証明
INV-15Deployment SafetyFeature flag + カナリア + 即時 rollback

2.2 v2 で追加(Codex 7回目指摘 #6 対応 5項目)

IDInvariantWhy this was missed in v1
INV-16Composite Vendor Availability複数ベンダ依存時の稼働率積算。例: Clerk 99.9% × Neon 99.95% × CF 99.99% = 99.84% → 月間 downtime 69分。単一 99.95% ベンダより悪化する可能性
INV-17Vendor Outage Fallback BehaviorClerk 障害時のログイン不可・Neon 障害時のデータ参照不可・Trigger.dev 障害時の予約投稿停止 に対する明示的な degraded mode 設計
INV-18Data Residency / Export Portability日本の個人情報保護法 + GDPR 相当:データ居住地の開示・顧客からのデータ export リクエスト対応
INV-19AI Audit LoggingClaude API への prompt / response をテナント単位で保存・redaction・retention(顧客苦情時の証跡)
INV-20API Compatibility / Versioning顧客が将来 API 連携する可能性への備え。破壊的変更時の deprecation policy

3. 候補案 Bill of Materials(17と同一、再掲)

案α Cloudflare all-in案β Supabase hybrid案γ Neon + Clerk
FrontendCF PagesCF PagesCF Pages
API GatewayCF Workers + HonoCF Workers + HonoCF Workers + Hono
AuthCF Access + 独自 sessionSupabase AuthClerk
一貫性境界Durable ObjectsPostgres advisory lockPostgres advisory lock
非同期ジョブCF Queuespg-bossTrigger.dev v3
CronWorkers Cronpg_cronTrigger.dev scheduled
MediaR2R2R2
DBD1 (SQLite)Supabase PostgresNeon (serverless PG)
Backupwrangler d1 export 日次PITR 7日 標準PITR 7日 + Branch
ObservabilityLogpush + SentrySupabase Logs + SentrySentry + Better Stack
SecretsWorkers SecretsSupabase Vault (pgsodium)§9 で詳述
AbuseCF WAF + TurnstileCF WAF + TurnstileCF WAF + Turnstile

4. スコアリング rubric + 評価(v2: rubric 追加)

v1 からの変更: Codex 7回目指摘 #4 対応で rubric を明示。「unknown」スコアを導入。disputed row(INV-1, INV-3, INV-8, INV-12)に evidence 列を追加。

4.1 スコアリング rubric

Score意味判定基準
5Nativeプラットフォーム標準機能が invariant をそのまま実装。prior art 多数・failure mode が既知
4Sound標準パターンで実装可能、ただしプラットフォーム固有の運用知識が必要
3Achievable実装可能だがカスタム設計が必要、failure mode に新規リスクあり
2Difficult実装可能だが大量のカスタム実装が必要、v3 で whack-a-mole が起きた領域
1Blockedプラットフォームが構造的に invariant を満たせない(launch-blocking)
?Unknown評価に必要な evidence 不足、prototype で確認が必要

4.2 Hard Gate vs Weighted Score

INV のうち Hard Gate(1つでも Score=1 なら launch 不可):

残り 16 項目は weighted total で総合評価。

4.3 スコア評価(20 invariant × 3案)

InvariantαβγEvidence / Rationale
INV-1 Auth actor355α: CF Access は店舗 auth に使えるが承認者サブアカウント + CSRF + MFA は自作。β/γ: Supabase Auth / Clerk は native 実装
INV-2 Tenant isolation355α: D1 で自前実装。β/γ: Postgres RLS が native
INV-3 Idempotent publish (Hard Gate)355α: DO は single-writer を提供するが、outbox pattern + IG Graph API reconciliation は自作。β/γ: Postgres tx + outbox table が教科書的パターン
INV-4 Bounded media444全案とも R2 direct upload で同等
INV-5 Durable audit355α: per-tenant AuditDO は実装可だが cold-start / backpressure が新規設計。β/γ: Postgres tx + append-only + hash chain が標準
INV-6 Single writer455α: DO は single writer 提供、β/γ: advisory lock 成熟
INV-7 Observable455β/γ: SQL で直接クエリ可能
INV-8 Recovery (Hard Gate)155D1 は PITR 非対応で RPO 5min 不達成。日次 export では RPO 24h が上限。β/γ: PITR 標準
INV-9 Abuse444全案とも CF WAF + Turnstile
INV-10 Migration rollback245γ: Neon Branch で本番同等環境での rollback rehearsal 可能
INV-11 Schema evolution255D1 は ALTER TABLE 制約多、Postgres は zero-downtime migration ツール(pg_trgm, pg_repack 等)成熟
INV-12 Backup/restore (Hard Gate)155D1 PITR 非対応で Hard Gate 失敗
INV-13 Secrets lifecycle (Hard Gate)344§9 で γ 専用設計を提示。β は pgsodium、γ は CF Workers Secrets + 独自 envelope encryption
INV-14 Retention/deletion355Postgres は DELETE + tombstone + audit が素直
INV-15 Deployment safety445Neon Branch で prod-like env
INV-16 Composite availability532α が最良: 単一ベンダなので composite 劣化なし。γ: Clerk 99.9% × Neon 99.95% × Trigger.dev 99.9% × CF 99.99% = 99.74%(月 113分 downtime 許容)
INV-17 Outage fallback333全案で degraded mode を自前設計する必要あり(read-only mode, publish queue freeze 等)
INV-18 Data residency???prototype で確認: CF は JP region 指定可、Supabase Tokyo region あり、Neon は AWS ap-northeast-1 ある
INV-19 AI audit355Postgres に jsonb で prompt/response 保存が素直、redaction は app 層で
INV-20 API versioning444全案とも Hono で /v1/ prefix ルーティング可能、同等

4.4 Hard Gate 判定

INV-3INV-8INV-12INV-13Hard Gate
α3113Failed(INV-8, INV-12)
β5554✅ Pass
γ5554✅ Pass

4.5 Weighted Score(Hard Gate 通過後の 16項目)

INV-18(?)を除外した 15項目で合計:

4.6 スコアからの結論

α は Hard Gate 失敗(D1 の PITR 非対応)で invariants を構造的に満たせない。 β と γ は weighted score で同点(67/75)。差別化は INV-10(migration rollback)と INV-16(composite availability)のトレードオフ:

5. 3年 TCO(v2: scenario ベース)

v1 からの変更: Codex 7回目指摘 #2 対応で 30/100/300店舗 × staff 1-5 の scenario 化。Clerk 料金前提を 1つに統一、incident cost 内訳を明示。

5.1 Clerk 料金前提の統一

Clerk Pro tier は 10,000 MAU まで $25/月、超過は $0.02/MAU。 店舗あたり staff 2-5 名想定(店長 + 現場スタッフ 1-4名)。

店舗数staff/店総 MAUClerk 月額
30260$25
305150$25
1002200$25
1005500$25
30051,500$25

結論: 想定範囲では Clerk は全て $25/月。v1 の「$25×100店舗 = $2,500/月」は MAU と店舗数を混同した誤り。本書は $25/月前提で統一

5.2 月額 infra(3 scenario)

項目30 店舗100 店舗300 店舗
CF Workers Paid$5$5$10(超過分)
CF Pages$0$0$0
CF R2$2$5$15
CF WAF$20$20$20
CF Turnstile$0$0$0
Neon Launch$19$19$69(Scale plan)
Clerk Pro$25$25$25(10k MAU 以内)
Trigger.dev$20$50$100
Sentry Team$26$26$26
Better Stack$24$24$24
Claude API$15$40$120
Staging/Dev 環境$15$20$30
月額計$171$234$439
店舗あたり単価$5.7$2.3$1.5

5.3 3年 TCO 比較(100 店舗想定)

項目α(参考・Hard Gate失敗)β Supabaseγ Neon+Clerk備考
Infra 3年合計(100店舗)$100×36=$3,600$209×36=$7,524$234×36=$8,424通常運用コスト
実装工数(¥8,000/h)N/A180h=¥1.44M180h=¥1.44M実装量は同等(Postgres パターンで近似)
運用工数(3年 10h/月 ¥8,000/h)N/A360h=¥2.88M360h=¥2.88M24/7 escalation 前提
Incident cost 推定(3年)N/A¥0.6M¥0.6M根拠: Codex 過去レビューで両案とも PITR 標準・outbox 設計済み。重大事故を年1回・復旧8h×¥8,000×2名=¥128k/回と想定
3年 TCO 合計N/A約 ¥6.0M約 ¥6.1M差は ¥100k(1.6%)

5.4 スケーリング感度(売上比)

店舗数売上/月Infra/月(γ)Infra 比率
30¥600,000$171 ≈ ¥25,6504.3%
100¥2,000,000$234 ≈ ¥35,1001.8%
300¥6,000,000$439 ≈ ¥65,8501.1%

スケールするほど比率が下がる(スケール利益が効く)。

5.5 TCO 結論

β と γ の TCO 差は 1.6%(¥100k/3年)。統計的に意味のある差ではない。コストは決定要因にならない。 決定要因は次章(§6)の非コスト要因に移る。

6. Decision

✅ 採用アーキテクチャ(v2 確定)

案γ — Neon + Clerk + Trigger.dev + R2 + CF Workers

Hard Gate 通過 / Weighted Score 67 / TCO ¥6.1M/3年 / Migration rollback 優位

6.1 γ を β より選ぶ根拠(コスト以外)

観点βγ勝者
Migration rollback(INV-10)45γ
Deployment safety(INV-15)45γ
Composite availability(INV-16)32β
Auth 機能(MFA / SSO / org 管理)Supabase Auth(basic)Clerk(pro-grade)γ
Postgres プロバイダの成熟度Supabase(自社運用)Neon(自社運用 + serverless)同等
Tokyo region 対応◎ (AWS ap-northeast-1)同等

6.2 γ 採用の 3大理由

  1. INV-10 Migration rollback が最重要: v2→v4 移行で失敗した場合の店舗単位ロールバック能力。Neon Branch は本番とほぼ同一環境でのリハーサルが可能
  2. Clerk の Auth が INV-1 を最も素直に実現: 店舗管理者 / 承認者 / admin の3 actor 階層、MFA、CSRF、session lifecycle が managed
  3. INV-16 劣位は INV-17(outage fallback)で補償可能: Clerk 障害時は既存 JWT 継続利用、Neon 障害時は read-only mode で既存データ閲覧、Trigger.dev 障害時は queue freeze。app 層の degraded mode で対処

6.3 INV-16 劣位の受容条件

Composite availability 99.74% ≒ 月間 113分 downtime 許容。これは SLO「月間 downtime < 2時間(120分)」の範囲内。SLO を満たせる境界内なので受容可能。

7. Rejected Alternatives

7.1 案α(Cloudflare all-in)

Reject 理由: INV-8 と INV-12 で Hard Gate 失敗(D1 PITR 非対応)。wrangler d1 export 日次では RPO 5min 不達成。

7.2 案β(Supabase)

次点、非採用。TCO 差は実質ない(¥100k/3年)。INV-10 Migration rollback で γ に劣る。子安氏が Neon/Clerk/Trigger.dev 不採用を強く推奨する場合のみ β に切替

7.3 案δ(Fly.io + Postgres monolith)

Reject 理由: container 常時起動コスト、単一リージョン依存、Workers 既存資産の廃棄コスト。

7.4 案ε(AWS Lambda + DynamoDB + SQS)

Reject 理由: DynamoDB single-table design の学習コスト、audit chain 実装の複雑化。

8. Assumptions / Consequences / Open Risks

8.1 Assumptions(採用の前提)

  1. 子安氏が Node.js + Postgres + Cloudflare Workers での SaaS 実装経験を持つ(or 学習 20h 以内)
  2. 店舗は通常業務時間(9-23時)メイン利用。深夜帯は低負荷
  3. Instagram Graph API の仕様が v4 リリース後 1年は大変更なし
  4. ¥35,000 上位プラン契約率 80% 以上(100店舗 ¥3.5M/月想定)
  5. 既存 v2 10店舗は v4 リリース後 6ヶ月以内に全店移行に同意
  6. Clerk / Neon / Trigger.dev の SLA(99.9% / 99.95% / 99.9%)が公称値通り

8.2 Positive Consequences

8.3 Negative Consequences

8.4 Migration Consequences

8.5 Open Risks(prototype phase で閉じる)

RiskMitigation閉じる evidence
Neon cold start が UX 悪化pgbouncer / Connection pooling / Warmup cronE8: p95 DB query < 300ms
Trigger.dev 本番 SLA 未検証1週間 prototype で retry storm 試験E4: IG 1h outage 後の復旧 10分以内
子安氏の習熟時間事前 tutorial 共有 + 1週目は CC 支援厚めWeek 1 の習熟レポート
IG Graph API idempotencyoutbox + creation_id + reconciliationE2: Worker crash test
INV-18 Data residency 未確認各ベンダの JP region 契約確認E9: 全 PII が JP region に留まる証明

9. γ 専用 Secrets 設計(INV-13 の具体実装)

v1 からの変更: Codex 7回目指摘 #1 (critical) 対応で γ 専用の vault/envelope encryption 設計を追加。v1 の pgsodium 参照(Supabase固有)は削除。

9.1 管理対象 secrets(4分類)

Class対象保管場所Rotation 周期
Platform secretsJWT signing key(Clerk 内蔵)Clerk managedClerk UI で 30日ごと(推奨)
Infrastructure secretsNeon DB URL / R2 access key / Trigger.dev API key / Sentry DSN / Anthropic API keyCloudflare Workers Secrets(wrangler secret put)90日ごと(手動)
Master Encryption Key(MEK)v4_MEK_V1, V2, ...(ローテーション用)Cloudflare Workers Secrets180日ごと(手動)
Per-tenant secrets各店舗の Instagram access_token / refresh_tokenNeon Postgres(MEKで envelope暗号化)IG 側 60日(F-06 Cron で自動)

9.2 Envelope Encryption Scheme

Instagram access_token を Neon に保存する時:

1. アプリ側で DEK (Data Encryption Key) を生成
   DEK = crypto.getRandomValues(32 bytes)  // AES-256

2. DEK で IG token を暗号化
   ciphertext = AES-GCM(DEK, IG_token, IV=12 random bytes)

3. MEK (Master Encryption Key, Cloudflare Workers Secrets から取得) で DEK を暗号化
   wrapped_DEK = AES-GCM(MEK, DEK, IV=12 random bytes)

4. Neon に保存
   INSERT INTO stores (id, ig_token_ciphertext, ig_token_iv, ig_token_wrapped_dek, ig_token_wrapped_dek_iv, ig_token_mek_version)
   VALUES (store_id, ciphertext, iv, wrapped_DEK, wrapped_DEK_iv, 'v1');

復号時:
1. mek_version 列から使用 MEK version を特定('v1' なら env.V4_MEK_V1)
2. MEK で wrapped_DEK を復号 → DEK
3. DEK で ciphertext を復号 → IG_token

9.3 MEK Rotation フロー

  1. 新 MEK V2 を生成し、wrangler secret put V4_MEK_V2
  2. バッチ cron で全 stores を読み取り、MEK V1 で wrapped_DEK を復号 → MEK V2 で再暗号化
  3. mek_version を 'v2' に更新
  4. 全 store の移行完了後、V4_MEK_V1 を削除

9.4 Revocation フロー

9.5 Audit

以下の操作を audit_logs に記録:

9.6 Restore 挙動

Neon PITR で DB を過去時点に restore した場合:

9.7 v2 からの Token Migration

v2 の IG token は AES-GCM で暗号化されているが、envelope pattern ではない。v4 移行時:

  1. v2 の復号ロジック(既存 services/crypto.ts)で plaintext 化
  2. v4 の envelope encryption で再暗号化
  3. Neon に保存
  4. v2 の暗号化済み token は削除

10. Evidence Failure Matrix(v2: Codex 指摘 #5 対応)

v1 からの変更: E2-E5 を failure injection matrix 化。crash timing / backlog size / outage type / rollback 基準を明示。

10.1 E1 Authz matrix

ActorActionExpected
Store manager (store A)Publish to store A200 OK
Store manager (store A)Publish to store B403 Forbidden
Approver (store A)Approve store A request200 OK
Approver (store A)Create post for store A403 Forbidden
AdminAny action on any store200 OK + audit log
UnauthenticatedAny authenticated endpoint401 Unauthorized

10.2 E2 Publish idempotency: Crash injection matrix

Crash timingExpected recovery
Before IG media_create API callNext cron retry, publish once
After IG media_create success but before creation_id savedReconciliation: query IG for orphan media_id, resume
After creation_id saved but before IG media_publishNext cron picks up creation_id, publishes
After IG media_publish success but before DB commitReconciliation: query IG, confirm published, update DB
After DB commit but before Trigger.dev ackTrigger.dev retries, second attempt sees status='published', skips
During reconciliation queryNext reconciliation cron completes

Pass 基準: 全 6ケースで Instagram 投稿は最終的に ちょうど1回、DB posts テーブルに ちょうど1行 status='published'

10.3 E3 Audit append: 100 並行書込

Scenario: 100 並行リクエストが同時に audit_logs 書込をトリガー

Pass 基準: advisory lock + tx でchain fork ゼロ、全 100行が prev_hash で連結、seq が 1〜100 の連番。

10.4 E4 Retry storm: 1h IG outage recovery matrix

Backlog sizeOutage typeExpected recovery
30 posts (30店舗想定)Mock IG API 500Outage 解除後 5分で全完了
100 posts (100店舗想定)Mock IG API 500Outage 解除後 10分で全完了
300 posts (300店舗想定)Mock IG API 500Outage 解除後 15分で全完了
100 postsMock IG rate limit (429)exponential backoff で 20分で全完了
100 postsTrigger.dev 自体の障害(mock)Trigger.dev 復旧後 5分で re-enqueue

10.5 E5 Migration dry-run matrix

DatasetTestPass 基準
v2 現本番 10店舗 snapshotv4 へ変換 + dual-read 検証1店舗 5分以内、posts 件数 diff ゼロ
100店舗合成データ(10倍)一括移行スクリプト全件 30分以内、ランダム 100件の内容一致
移行中に v2 へ新規投稿Dual-write が v4 にも反映されるか最大 10秒遅延で同期
Rollback: v4 → v2 へ戻すNeon Branch を活用1店舗 10分以内、データ喪失ゼロ

10.6 E6 Observability

Publish 失敗時に Slack 通知が 60秒以内に到達。Sentry に exception 記録。Better Stack で downtime 計測。

10.7 E7 Operator debug

任意の post の audit trail を SQL 1クエリで 1分以内に取得。

10.8 E8 Neon latency

CF Workers から Neon への p95 query latency < 300ms(cold start 除外時)。cold start 込みで p95 < 3秒。

10.9 E9 Data residency

Neon、Clerk、Trigger.dev、R2 の全ベンダで設定した region が日本(ap-northeast-1 or Tokyo)であり、PII が region を出ないことを管理画面で証明。

11. Delivery Plan

11.1 Phase 1 MVP(Week 3〜Week 14 = 12週間)

11.2 Phase 2

11.3 Phase 3

11.4 Phase 0: ADR合意 → prototype(Week 1〜2)

12. Operating Model(v2: 24/7 採用)

v1 からの変更: Codex 7回目指摘 #3 対応。SLO 99.5% / RTO 1h と整合する 24/7 escalation を明示。

12.1 SLO(再掲)

12.2 On-call Rota(24/7)

時間帯PrimarySecondary応答時間
平日 9-19時子安氏高木氏15分以内
平日 19時-翌9時子安氏(on-call 手当付き)高木氏30分以内
土日祝当番制(子安氏・高木氏交替)もう一方30分以内

12.3 Alert Severity と通知先

Severityイベント例通知先対応
P1(即時)Audit chain fork / DB breach / 全店舗 publish 不能Slack #incident + SMS(Better Stack)30分以内に primary 応答
P2(1時間以内)予約投稿失敗 >10件 / Cron 遅延 >15分 / Clerk or Neon or Trigger.dev 障害Slack #ops + Email1時間以内に primary 応答
P3(翌営業日)個別投稿失敗 / login スパイク / IG token 期限 48h 前Slack #ops翌営業日対応

12.4 Runbooks(作成必須)

12.5 Vendor Status Monitoring

Better Stack で以下を 1分間隔で監視:

12.6 Customer Communication

12.7 月次 Drill

13. 子安氏への依頼事項

✅ Q1-Q4 + blocking concerns 自由記述欄

Codex 7回目指摘 #8 対応で、closed 4問 + open 欄を用意。Q1 = Yes でも prototype に入る前に Q5(blocking concerns)を読み合わせる。

#質問回答形式
Q1Decision(6章)の案γ で進めて良いかYes / No(No なら β or 代替案)
Q2Invariants 20項目に追加・削除・優先度変更はあるか追加リスト or「なし」
Q3Evidence(10章)E1〜E9 の pass 基準に修正はあるか修正リスト or「なし」
Q4Delivery Plan(11章)MVP を 12週間で実装可能かYes / No(No なら現実工期)
Q5Blocking concerns 自由記述(Q1-Q4 を Yes と答えても気になる点があれば)自由記述

13.1 進め方

14. 付録:関連ドキュメント