SYNON 記事 社内サービス開発
社内アプリ配信基盤 / 設計書

アプリハブ
— Cowork でつくり、ポータルから配る仕組み

作成日: 2026-08-28 / シンオン株式会社

対象: 用意する人(エンジニア・情シス)/ 決める人(経営)/ つくる人(非エンジニア社員)

要旨: 非エンジニアが Cowork で実装 → zip をポータルに上げるだけで、GitHub → Actions → Workers / D1 / R2 に配信される

成果物: 本書(md / html)と 実装一式 synon-apphub-kit.zip

第 I 部 — 全体像決める人・用意する人 共通/所要 15 分

1この文書の位置づけ — 4 つ目の方式全員

同じフォルダに、社内サービスを非エンジニアに作らせるための資料が 3 本ある。この文書は 4 つ目の方式の設計書であり、既存 3 方式を置き換えるものではない。

プラン Aプラン BCloudflare OSアプリハブ(本書)
つくる場所Codespaces本人の Mac(Claude Code)ブラウザCowork(ブラウザ)
GitHub アカウント必要不要(Deploy key)不要不要
PC への導入ゼロ30 分/人ゼロゼロ
ローカルでビルドありありなしなし
本番公開の関門PR マージPR マージなし(共有=即配布)ポータルの承認ボタン
差分レビューできるできる実質不可できる(画面上で)
退職者 PC の複製なし残る(残課題)なしなし
月額$50〜80$13$5+AI 従量$13
要点

アプリハブは「プラン B の配信経路だけを、人手からポータルに置き換えたもの」である。作るもの(Workers + D1 + R2)も、置き場所(GitHub)も、認証(Cloudflare Access)もプラン B と同じ。変えたのは、つくる人と GitHub のあいだに人間が立たなくてよくしたことの一点に尽きる。

あなたの立場読むところ所要
決める人1〜5 章と 24〜25 章15 分
用意する人(実装者)全部 + 実装一式の APPLY.md60 分
つくる人6〜13 章だけ。第 III 部は読まなくてよい15 分

2何を解くのか — プラン B の残課題用意する人

プラン B(claude-code-internal-app-workflow-planb)は動く設計だが、実装を進める中で 4 つの摩擦が残った。アプリハブはこの 4 つを潰すためだけに存在する。

#プラン B の残課題なぜ痛いかアプリハブでの解
1端末セットアップ 30 分/人Node・npm・Claude Code・Deploy key・launchd。1 人増えるたびに情シスが張り付く。社内プロキシ下で npm ci が通るかは実測しないと分からないPC に何も入れない。Cowork のブラウザだけ。ビルドは GitHub Actions の中でしか起きない
2退職者の Mac にソースの複製が残る技術で解けず「端末返却を退職手続きに紐づける」という運用の宿題になっていた正本は GitHub にしかない。手元は作業用の写し。Cowork のプロジェクトを消せば終わり
3公開のたびにエンジニアが PR を作る「1 回 2 分」は嘘ではないが、待ち時間が発生する。承認者が席にいないと止まるポータルの[本番公開]ボタン。GitHub を開かずに承認でき、記録も残る
4配信結果が本人に返らないgh が使えず Chatwork 通知で代替。失敗時に何が起きたか本人が読めないポータルに履歴とエラー本文が出る。Cowork にそのまま貼れる

プラン B から引き継いで変えないもの:ベースは cf-internal-app-starter(Workers + D1 + R2 + KV + Access)/ Cloudflare の鍵は GitHub Actions にしか置かない / 本番は必ず人が承認する / ブランチは試用と本番を分ける。

この基盤が向かないもの

常駐プロセス・重いバッチ・独自 DB・社内 LAN 内の機器を叩くもの・数百 MB のファイルを扱うものは作れない。既存資料の判定表でいう D4 に当たるものは対象外。従来どおりエンジニアが作る。

33 分でわかる全体像全員

工程は 8 つ。このうちつくる人がやるのは 3 つ(②③④)だけである。

一度きり 管理者・3 分 6 章 ① 新規アプリをつくる ポータルで 6 項目 →[作成] 裏で 9 つのことが自動で起きる GitHub リポジトリ + D1 + R2 + ドメイン + Access + 空アプリの初回配信 毎回まわる輪 つくる人 ブラウザだけ 30 分〜半日 7〜10 章 ② 受け取る プロジェクト zip ③ つくる Cowork で実装 ④ 上げる zip を投げる 差分プレビュー +12 変更 3 削除 1 [この内容で上げる] ⑤ 確かめる — 試用環境(約 2 分後) <slug>-stg.apps.synon.co.jp 直す ときどき 承認者・エンジニア ⑥ 本番公開(ボタン 1 つ) ⑦ 直す・戻す(版を選ぶ) ⑧ やめる(休止 → 1 年 → 削除) ★ つくる人の PC には Node も npm も Git も鍵も入らない。ブラウザだけ。 ★「動かして確かめる」は手元ではなく試用環境で行う。これが最大の設計判断。
図 1 — 全体像。工程 8 つのうち、つくる人がやるのは ②③④ の 3 つだけ
この図で一番大事なこと

「④ 上げる」と「⑤ 確かめる」のあいだに 2 分の待ちがある。プラン B ではローカルで npm run dev が回るぶん、手元で秒単位に確かめられた。アプリハブはそれを捨て、代わりにつくる人の PC から開発環境を丸ごと消した。この取引を受け入れられるかが、この方式を採るかどうかの分かれ目になる(→ 28 章 撤退基準)。

4設計の 3 原則用意する人

実装で迷ったときは、この 3 つに戻って決める。

原則 1 — 正本は GitHub にしかない

つくる人の手元のフォルダも、アップロードされた zip も、すべて写しである。消えても失われるものは無い。

原則 2 — 危ないものはつくる人に渡さない

渡さないもの: Cloudflare の API トークン、Access の設定、wrangler.jsonc.github/workflows/、本番の DB、他人のアプリ。

誤解しないこと

「悪意」への対策ではなく「AI が気を利かせて deploy.yml を書き換える」への対策である。実際、Claude に「デプロイが失敗する」と言えば deploy.yml を直そうとする。それを構造的に禁止する。

原則 3 — 確かめる場所は試用環境(staging)

手元でビルドできない代わりに、アプリごとに試用環境を常設する。本番と同じ構成・同じ認証・別の DB。

5登場人物と権限全員

役割は 4 つ。ポータルの中の役割であって、Cloudflare Access のグループとは別物である点に注意(Access は「この URL を開いてよいか」だけを見る)。

役割ポータルでできることできないこと
管理者情シス・エンジニアアプリ枠の作成/廃止、上限の変更、全アプリの閲覧、担当者の割当
つくる人非エンジニア社員自分が担当のアプリの zip ダウンロード/アップロード、試用環境への配信、配信履歴の閲覧本番公開ボタンは押せない。他人のアプリは見えない
承認者エンジニア または 部門長担当アプリの本番公開、ロールバック、差分の確認アプリ枠の作成・削除
使う人全社員出来上がったアプリを使う(一覧から開く)管理機能は見えない
プラン B からの変更点

承認者を「エンジニア」に固定しない。差分プレビューが画面上に出るので、業務の妥当性だけを見ればよい場合は部門長でよい。ただし data/migrations/ に破壊的な変更(DROP TABLE 等)が含まれる回だけは、エンジニアの承認を必須にする(→ 23 章)。

役割はポータルの D1 に持つ。Cloudflare Access は「ポータルに入れる社員かどうか」までしか見ない。役割の判定はポータル側の責任である。

第 II 部 — 8 つの工程画面で何が起き、裏で何が起きるか/つくる人が読むのはこの部だけでよい

6工程 1管理者:アプリ枠をつくる管理者

画面

ポータル https://apps.synon.co.jp/admin/new で 6 項目を入れて[作成]。

入力項目用途
アプリ名(日本語)見積書つくるくん一覧・通知の表示名
slug(半角英数とハイフン)mitsumoriリポジトリ名・ドメイン・DB 名の全部に使う
担当者(つくる人)田中この人だけが zip を上げられる
承認者大山この人だけが本番公開できる
使うもの☑ 表(D1) ☑ ファイル(R2) ☐ 早見表(KV) ☐ AI払い出す資源と wrangler.jsonc の中身が変わる
見せる範囲全社/営業部/指定メンバーAccess ポリシーになる
slug は後から変えられない

変えるとドメイン・DB 名・リポジトリ名が全部ずれる。作成画面で「あとから変えられません」と赤字で出す。

裏で起きること

[作成]を押してから、つくる人に zip が渡るまで 9 手順・所要 40〜90 秒。ポータルは Queue に積んで即座に「準備中」を返し、進捗を画面で更新する。

[作成]を押す 1 D1 に apps 行を作るstatus = provisioning 2 GitHub:空のプライベートリポジトリを作る POST /orgs/synon/repos name = app-mitsumori ★ テンプレートからの generate は使わない(20 章 落とし穴 1) 3 Cloudflare:資源を払い出す(本番と試用の 2 セット) D1 app-mitsumori-db / -db-stg R2 app-mitsumori-files / -files-stg KV(使う場合のみ) → 返ってきた ID を apps 行に保存 4 テンプレートを組み立てる(メモリ上) apphub-template の現行版 + app.config.json(入力値) + .appmeta.json + data/(空の migrations と seed) 5 GitHub:初回コミットを Git Data API で作る blobs → tree → commit → refs/heads/main → staging 6 GitHub:org secret の対象リポジトリに追加 PUT /orgs/synon/actions/secrets/CF_API_TOKEN/repositories/{id} ※暗号化不要 7 初回デプロイを起こす workflows/deploy.yml/dispatches 8 ドメインと入口をつくる Custom Domain 2 件 + Access アプリ 2 件 9 status = ready / 担当者へ Chatwork 通知 「見積書つくるくん の枠を用意しました。ここから始めてください →(URL)」
図 2 — [作成]の裏で起きる 9 手順。8 を 7 より後ろに置いているのは意図的
8 を 7 より後ろに置く理由

Worker が存在しない状態で Custom Domain を作ろうとすると失敗する。必ず 1 回デプロイが成功してからドメインを張る。

途中で失敗したとき

status = failed にして、作った資源は消さずに残す。同じ slug で作り直すと衝突するため、管理者画面に[この枠を撤去する]ボタンを別に用意し、そこから逆順で削除する。自動ロールバックは実装しない(消し間違いのほうが怖い)。

完了条件

7工程 2つくる人:受け取るつくる人

ポータルのアプリ詳細画面の[プロジェクトを受け取る]を押すと zip が落ちる。

中身は「テンプレートの控え」ではない

ダウンロードされる zip は、GitHub の staging ブランチの現在の中身そのものである。だから 2 回目以降にダウンロードすれば、自分が前回上げたものが返ってくる。

mitsumori.zip
├── PROJECT.md              ← ★まずこれを Cowork に読ませる(作り方の説明書)
├── app.config.json         ← つくる人が触れる唯一の設定
├── .appmeta.json           ← slug とリポジトリ名(触らない)
├── data/                   ← ★データの置き場(→ 15 章)
│   ├── data.md                 表の作り方の説明
│   ├── migrations/
│   │   └── 0001_init.sql       表の定義。ここに書いたものが配信先に作られる
│   └── seed/
│       └── .gitkeep            初期データ(CSV / SQL)を置くと初回だけ入る
├── src/
│   ├── client/             ← 画面(React)
│   │   ├── App.tsx  main.tsx  index.html  styles.css
│   └── worker/             ← サーバ(API)
│       ├── index.ts
│       ├── lib/access.ts       ログインした人が誰かを取り出す(触らない)
│       ├── lib/db.ts           表を読み書きする道具
│       └── routes/sample.ts    お手本の API 1 本
├── docs/
│   └── SPEC.md             ← /service-spec が書き込む仕様書
└── .claude/
    ├── skills/             ← Cowork のスキル 4 本
    │   ├── service-spec/       何を作るかを決める
    │   ├── service-build/      仕様書どおりに実装する
    │   ├── service-pack/       アップロード用 zip をつくる
    │   └── service-fix/        配信が失敗したときに直す
    └── CLAUDE.md           ← Claude への恒久的な指示(触らない)

zip に入っていないものnode_modules/.git/wrangler.jsonc.github/package-lock.json、実データ。理由は 15 章16 章で説明する。つくる人はこれらの存在を知らなくてよい。

8工程 3つくる人:Cowork で実装するつくる人

  1. Cowork で新しいプロジェクトを作る(プロジェクト名 = アプリ名)
  2. zip を展開したフォルダを、そのプロジェクトの作業フォルダとして接続する
  3. Claude に「PROJECT.md を読んで」と言う。以降はスキルが誘導する

スキル 4 本の役割

スキルいつ使う何をする
/service-spec最初に 1 回質問に答えていくと docs/SPEC.md ができる。「誰に何が見えてよいか」を最初に書かせるのが要点
/service-build実装するときdocs/SPEC.md を読み、表 → API → 画面 の順に作る
/service-pack上げる前除外ルールに沿って zip を作る。構文チェックと禁止事項チェックもここで通す
/service-fix配信が失敗したときポータルからコピーしたエラー本文を貼ると、原因の行を直す

Cowork でできること・できないこと

できるか補足
ファイルを読む・書く・検索する接続フォルダ上で普通に動く
Claude に実装させるここが本命
npm install / npm run dev×接続フォルダのシェルはネットワーク非接続・1 回約 45 秒の上限。この基盤はそもそも必要としない
ブラウザで動きを見る×(手元では)④ で上げて ⑤ の試用環境で見る
ファイルを削除する既定では不可。_to_delete/ に移す運用(Claude が案内する)
つくる人が最初に戸惑うところ

「作ったのに動かして見られない」。ここで挫折させないために、PROJECT.md の冒頭に 「手元では動きません。上げてから見ます。1 回 2 分です」と大きく書く。1 週目の実測でここが最大のつまずきになるはずなので、早見表に必ず追記する。

9工程 4つくる人:詰めて上げるつくる人

詰める(/service-pack

$ /service-pack

  ✔ 構文チェック(TypeScript / SQL / JSON)… OK
  ✔ 禁止事項チェック … OK
  ✔ 保護パスは含めない … OK(4 ファイルを除外)
  ✔ サイズ … 312 KB / 上限 20 MB
  ✔ ファイル数 … 38 / 上限 500

  できました: upload/mitsumori-20260828-1642.zip
  ポータルの[新しい版を上げる]に、この zip を投げてください。

構文チェックをローカルでやるのがこのスキルの存在意義である。ビルドは Actions でしか回らないので、明らかな構文エラーを 2 分待ってから知るのは無駄が大きい。tsc は使えない(node_modules が無い)ので、Claude 自身に読ませて確認させるという素朴な方法を採る。完全ではないが、括弧の閉じ忘れレベルは拾える。

上げる

つくる人 ポータル (Worker) Queue GitHub GitHub Actions → Cloudflare ① zip を選ぶ ② 入口の検査 ・担当者本人か ・20MB 以下か ・zip として開けるか ③ R2 に原本を保存 ④ 中身の検査(22 項目) ⑤ 差分プレビュー +12 変更 3 削除 1 ⑥[この内容で上げる] (即 202 が返る) ⑦ ジョブを積む ⑧ 全置換コミット → staging ⑨ push で起動 ⑩ ビルド → migrations → 配信 → 疎通確認 ⑪ 結果(ポータルの画面 + Chatwork)— webhook で受ける
図 3 — zip アップロードの経路。⑤ の差分プレビューがこの設計で最も効く安全装置
差分プレビュー(⑤)が要る理由

zip は全置換でコミットされる。/service-pack の除外ルールを間違えると ファイルが黙って消える。「削除 1」と出たときに「あれ?」と気づけるかどうかで事故の数が変わる。

  • 削除が 1 件でもあれば、確認欄に 削除されるファイル名を全部並べる
  • 削除が 10 件を超える、または data/ のファイルが削除される場合は、チェックを入れないと[上げる]が押せない

画面は「配信中(1/3 コミット済み)」のように進む。約 2 分で「試用環境に出ました」になる。失敗したら赤くなり、Actions のログの末尾 60 行がそのまま画面に出る。ここをコピーして /service-fix に貼るのが直しかたの導線。

10工程 5試用環境で確かめるつくる人

https://<slug>-stg.apps.synon.co.jp を開く。確かめることは 6 点(既存資料と同じ 6 点を使う)。

#確かめることこれを飛ばすとどうなるか
1入れるか(Access のログインを通るか)
2作った画面が出るか
3ブラウザを閉じて開き直しても、入れたデータが残っているか「画面の中だけで保持していて、リロードで消える」実装が本番まで届く
4変な値(空欄・全角数字・とても長い文字列)で壊れないか使い始めた初日に落ちる
5スマホで開いて読めるか
6同僚 1 人に URL を渡して、説明なしで使えるか「作った本人しか使えないもの」が量産される

3 と 6 は必須。この 2 つが、この基盤で最も多い 2 種類の失敗をそれぞれ潰す。

11工程 6承認者:本番公開承認者

ポータルのアプリ詳細に、承認者にだけ[本番公開]ボタンが出る。押す前に見えるもの:

[本番公開]を押すと、ポータルが GitHub の stagingmain にマージする(merge API)。マージは GitHub App のトークンで行うため、main への push を待つ Actions が起動する(→ 20 章 落とし穴 3)。約 2 分で本番に出る。

PR は作らない

差分の確認はポータルの画面で終わっている。GitHub 上のレビューが必要な回(migrations の破壊的変更など)だけ、ポータルが PR を作ってエンジニアに投げる、という二段構えにする。

承認の記録

誰がいつ何を公開したかは D1 の deployments に残る。この記録があることが「非エンジニアが本番に触れる」ことの唯一の担保なので、削除できない設計にする(監査ログは追記のみ)。

12工程 7直す・戻す全員

状況やること
試用環境で不具合を見つけた③ に戻って直し、また上げる。試用環境は何度上げてもよい(承認不要)
本番に出したあとで不具合を見つけたポータルの[1 つ前に戻す]。前の版の tree でコミットを作り直して再配信する。約 2 分
Cowork のプロジェクトを消してしまった[プロジェクトを受け取る]で最新版を落とし直す。手元は写しなので何も失われない
別の人が引き継ぐ管理者が担当者を付け替える。新担当者が zip を落とすところから始める
どの版に戻せるか知りたい配信履歴に全版が並ぶ(コミット SHA・日時・実行者・差分)
ロールバックの方式

「前の tree で新しいコミットを作る」方式にするのは、履歴を書き換えないためgit revert 相当の操作をポータルが行う。force push は一切しない。

13工程 8やめる管理者

使われなくなったアプリの畳みかた。「消す」を最後に置き、途中で止められるようにする。

段階操作元に戻せるか
1休止ポータルで[休止]。Access のポリシーを「管理者のみ」に変更。一覧から消える○ ボタン 1 つ
2データの書き出しD1 の全テーブルを CSV に、R2 の全ファイルを zip にして保管領域へ
3資源の停止Custom Domain を外し、Worker を削除。D1 と R2 は残す△ 再デプロイで戻る
4アーカイブGitHub リポジトリを archive。D1・R2 は 1 年保持
5削除1 年後に D1・R2・リポジトリを削除×
3 で D1 と R2 を残すのが要点

「もう使わない」と言われたアプリの数字を、半年後に誰かが探しに来る。

第 III 部 — 設計実装する人のための章/つくる人は読まなくてよい

14リポジトリとブランチ用意する人

リポジトリ中身誰が触る
synon/apphubポータル本体(Worker + 管理画面 + Queue コンシューマ)エンジニアのみ
synon/apphub-templateテンプレートの正本。ここを直すと以後の新規アプリに反映されるエンジニアのみ
synon/app-<slug>アプリ 1 本につき 1 リポジトリ。プライベートポータルが自動で書く

アプリを 1 リポジトリ 1 本にするのは、権限とロールバックを単純にするためである。モノレポにすると「A さんの変更で B さんのアプリが落ちる」が起き、差分プレビューも複雑になる。

テンプレート改善が既存アプリに流れない問題(プラン B からの引き継ぎ)

テンプレートを直しても、既に作られたアプリには自動で反映されない — アプリハブでは反映できる。保護パス(16 章)はポータルが毎回テンプレートの現行版で上書きするので、.github/workflows/deploy.yml.claude/ の改善は、次に誰かが zip を上げた瞬間に全アプリへ広がる。

逆に src/ の共通コードは広がらない。共通化したいものは保護パスに入れるか、npm パッケージにする。この判断を最初に決めておくこと。

main 本番<slug>.apps.synon.co.jp staging merge merge merge 試用<slug>-stg.apps.synon.co.jp zipzipzipzip zipzipzipzipzip ・main への push はポータル(GitHub App)だけ。branch protection で人間の直接 push を禁止する ・work ブランチは作らない(自動保存が存在しないため)。Deploy key も使わない(端末に鍵を配らない)
図 4 — ブランチ 2 本。[本番公開]だけが main を動かす

15★ データの外出し設計用意する人

「テンプレートではデータフォルダを外出しにして、配信先で初期作成される設計」を具体化する。この章がこの設計書で最も重要である。

なぜ外出しするのか — 理由はひとつ

zip を上げても本番のデータが消えないようにするため。これに尽きる。

zip は全置換でコミットされる。もし実データがリポジトリに入っていたら、つくる人が古い zip を上げた瞬間に本番のデータが 3 日前に巻き戻る。そんな仕組みは 1 回目の事故で使われなくなる。

リポジトリ(GitHub)に入る = 設計図 data/migrations/0001_init.sql表の定義(CREATE TABLE …) data/migrations/0002_add.sql表の変更(ALTER TABLE …) data/seed/master.csv初期マスタ(都道府県一覧など) data/data.mdつくる人向けの説明 src/ docs/ app.config.jsonコードと仕様書 配信のたびに Actions が適用する 配信先(Cloudflare)にできる = 実データ D1 app-<slug>-dbmigrations が作った表。中身は業務データ R2 app-<slug>-files利用者がアップロードしたファイル KV app-<slug>-cache一時的な値(消えてよいもの) ★ ここにあるものは zip に入らない。zip を上げても消えない。 ★ 本番と試用で別々。試用でデータを全部消しても本番は無事。 つくる人の手元(Cowork の接続フォルダ) .data/local.sqlite ローカル実験用。.gitignore 済み。zip にも入らない (この基盤では手元でアプリを動かさないので、通常はこれも生まれない)
図 5 — リポジトリには設計図だけ、実データは配信先だけ。zip の往復でデータは動かない

「配信先で初期作成される」の実装

3 段階で作られる。すべて冪等(何回やっても同じ結果)にするのが設計条件。

いつ誰が何が作られる冪等性の担保
枠を作るときポータル(Cloudflare API)D1 データベース本体(空)、R2 バケット、KV 名前空間既に同名があれば作らず ID を取得
毎回の配信Actions(wrangler d1 migrations apply表・索引。未適用の migration だけが流れるD1 の d1_migrations テーブル
初回配信だけActions(seed スクリプト)初期データ_apphub_seeds にファイル名を記録し、記録済みは飛ばす
-- テンプレートの data/migrations/0000_apphub.sql(保護パス。つくる人は触らない)
CREATE TABLE IF NOT EXISTS _apphub_seeds (
  file       TEXT PRIMARY KEY,
  applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);

つくる人に見せるルール(data/data.md の中身)

  1. 表を作りたい/項目を足したいときは data/migrations/ に新しいファイルを足す。既にあるファイルは直さない
  2. ファイル名は 0002_やること.sql のように 4 桁の番号 + 内容。番号は前より大きく
  3. 表を消す・項目を消す SQL を書くと、公開のときにエンジニアの承認が要る(データが消えるため)
  4. 初期データを入れたいときは data/seed/ に CSV を置く。初回の 1 回だけ入る
  5. 実際の業務データはここには置かない。置いても配信先には反映されず、しかも社内の記録に残り続ける
5 が一番大事

「試しに本物の顧客リストを data/seed/ に置く」が最も起きやすい事故である。

  • /service-packdata/seed/ の CSV が 100 行を超えたら警告を出す
  • メールアドレスらしき文字列や 11 桁の数字の並びを検出したら、アップロードを保留して人に確認させる(→ 23 章 検査 18・19

本番と試用でデータを分ける

本番試用
D1app-<slug>-dbapp-<slug>-db-stg
R2app-<slug>-filesapp-<slug>-files-stg
migrations適用する先に適用する(試用で失敗したら本番には行かない)
中身業務データseed だけ。本番のコピーは入れない
必ず言われること

「試用に本番のデータを入れてほしい」は必ず言われる。断る。どうしても必要なら、管理者が個人情報を落としたダンプを作って入れる運用にし、つくる人の操作ではできないようにする。

16保護パスと生成物用意する人

アップロードされた zip の中のファイルは、パスによって 3 通りに扱われる。

① つくる人のもの src/** docs/** data/migrations/** data/seed/** public/** app.config.json README.md zip の中身をそのまま採用 ② 保護パス(テンプレート正本で上書き) .github/**配信の手順そのもの .claude/CLAUDE.md .claude/settings.json .claude/skills/** data/migrations/0000_apphub.sql src/worker/lib/access.ts認証。壊すと全公開になる .gitignore tsconfig.json vite.config.ts scripts/** .appmeta.json allowlist.json zip に入っていても無視し、 テンプレートの現行版で置き換える → テンプレート改善が   自動で全アプリに広がる ③ 生成物(zip にもリポジトリにも人が書かない) wrangler.jsoncapp.config.json から Actions が生成 package.jsonapp.config.json の packages から生成 package-lock.json dist/ node_modules/ .data/
図 6 — 3 分類。② があることで、つくる人(と AI)が配信の仕組みを壊せない

なぜ wrangler.jsonc を生成物にするのか

理由

wrangler.jsonc はこのアプリが触ってよい資源の一覧そのものだからである。ここに他人の D1 の ID を書けば、他人のデータが読める。つくる人(と Claude)に編集させてはいけない。

  app.config.json  ─┐
                    ├─→ scripts/gen-wrangler.mjs ─→ wrangler.jsonc ─→ wrangler deploy
  Actions variables ┘        (allowlist で検証)
   D1_ID / D1_ID_STG / R2_NAME / KV_ID / ACCESS_AUD …

生成に失敗したら配信は止まる。app.config.json に allowlist 外の値が書いてあったら、その場で落として理由を画面に出す。

保護パスの上書きはいつ起きるか

ポータルがコミットを組み立てるときに、毎回。zip の中身を無視して synon/apphub-template の現行 main の該当ファイルを読み、それをコミットに含める。

17app.config.json 仕様用意する人

つくる人(と Claude)が触れる唯一の設定ファイル。

{
  // 表示名。ポータルの一覧とアプリのタイトルに出る
  "name": "見積書つくるくん",

  // 使うもの。false にすると wrangler.jsonc から丸ごと消える
  "uses": {
    "d1": true,      // 表(データベース)
    "r2": true,      // ファイルの保管
    "kv": false,     // 早見表(キャッシュ)
    "ai": false      // Workers AI(従量課金なので既定 false)
  },

  // 定期実行。cron 式。最大 3 本
  "cron": ["0 0 * * *"],   // UTC 0:00 = 日本時間 9:00

  // 追加で使う npm パッケージ。allowlist 内のみ
  "packages": ["zod", "date-fns"],

  // 見せる範囲。Access のポリシーになる。ポータル側でも検証する
  "visibleTo": ["全社"],

  // 配信結果の通知先(Chatwork のルーム ID)
  "notify": { "chatwork": "123456789" },

  // アップロード可能なファイルの上限(R2 を使う場合)
  "upload": { "maxMb": 10, "allow": ["pdf", "png", "jpg", "xlsx", "csv"] }
}

packages の allowlist

任意の npm を許さない理由

依存に何が入るか誰も見ていない。許した瞬間に、この基盤の安全性の前提が全部崩れる。

初期の allowlist は次の 10 個だけにし、追加要望はエンジニアが 1 件ずつ判断する。

パッケージ用途パッケージ用途
zod入力チェックpapaparseCSV の読み書き
date-fns日付の計算・書式xlsxExcel の読み書き
honoルーティング(テンプレートが使用)markedMarkdown の表示
nanoidID の採番chart.jsグラフ
clsxクラス名の組み立て@cloudflare/workers-types型定義
allowlist は 1 か所にしか置かない

ポータルの検査と Actions の生成の両方が同じ表を見る。2 か所に持つと必ずずれるので、テンプレートリポジトリの allowlist.json を正本とし、ポータルはそれを読んで 5 分キャッシュする。

18ポータルのアーキテクチャ用意する人

社員のブラウザ Cloudflare Access(Google Workspace) Cf-Access-Jwt-Assertion Worker synon-apphub apps.synon.co.jp Static Assets管理画面(単一 HTML・素の JS) API /api/apps一覧・作成・詳細 …/downloadstaging を zip で返す …/uploads受ける → 検査 → 差分 …/commit確定 → ジョブ投入 …/promote本番公開(承認者のみ) …/rollback版を戻す /api/webhooks/*Actions の結果を受ける Bindings D1 APPHUB_DB アプリ台帳・履歴・監査 R2 ARTIFACTS 受け取った zip の原本(90 日) KV CACHE GitHub トークン・テンプレート正本 Queue JOBS 重い処理を後ろに回す Secrets Store enqueue Queue コンシューマ ・zip を展開/保護パスを差し替え ・blob → tree → commit を作る ・Actions を起こす ・資源を払い出す(新規のとき) GitHub synon/app-* Actions が Cloudflare に配信する webhook Cloudflare Workers(アプリ本体) D1(表) R2(ファイル) KV(早見表) Custom Domain Access アプリ ↑ ポータルが Cloudflare API   で払い出し、Actions が配信
図 7 — ポータルの内部構成。重い処理は Queue の向こう側にある

なぜ Queue を挟むのか

zip 1 本のコミットは ファイル数前後の GitHub API 呼び出し(blob 作成 → tree → commit → ref 更新)になる。40 ファイルで 43 回。ネットワーク待ちが支配的で、リクエストの中で全部やると利用者を 20〜40 秒待たせる。

管理画面を単一 HTML にする理由

ポータル自体をビルドが要る構成にすると、ポータルを直すのが億劫になる。画面は 1 枚の HTML(フレームワーク無し・素の JS)にし、Static Assets で配る。1,500 行程度に収まる。

19ポータルのデータモデル用意する人

-- アプリ台帳
CREATE TABLE apps (
  id            TEXT PRIMARY KEY,            -- ULID
  slug          TEXT NOT NULL UNIQUE,        -- mitsumori
  name          TEXT NOT NULL,
  status        TEXT NOT NULL,               -- provisioning|ready|failed|suspended|archived
  owner_email   TEXT NOT NULL,               -- つくる人
  approver_email TEXT NOT NULL,              -- 承認者
  repo_full     TEXT,                        -- synon/app-mitsumori
  repo_id       INTEGER,                     -- GitHub の数値 ID(org secret 付与に使う)
  uses_json     TEXT NOT NULL,
  visible_to    TEXT NOT NULL,
  cf_json       TEXT,                        -- 払い出した資源の ID
  prod_url      TEXT, stg_url TEXT,
  created_by    TEXT NOT NULL,
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

-- アップロードされた zip 1 回分
CREATE TABLE uploads (
  id          TEXT PRIMARY KEY,
  app_id      TEXT NOT NULL REFERENCES apps(id),
  uploader    TEXT NOT NULL,
  r2_key      TEXT NOT NULL,                 -- uploads/<app>/<id>.zip
  bytes       INTEGER NOT NULL,
  file_count  INTEGER NOT NULL,
  diff_json   TEXT NOT NULL,                 -- {added:[],changed:[],deleted:[]}
  verdict     TEXT NOT NULL,                 -- pending|ok|hold|accepted|rejected
  reject_reason TEXT,
  commit_sha  TEXT,
  created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

-- 配信 1 回分(staging も本番も)
CREATE TABLE deployments (
  id          TEXT PRIMARY KEY,
  app_id      TEXT NOT NULL REFERENCES apps(id),
  env         TEXT NOT NULL,                 -- staging|production
  commit_sha  TEXT NOT NULL,
  tree_sha    TEXT NOT NULL,                 -- ★ロールバック用
  run_id      INTEGER,
  status      TEXT NOT NULL,                 -- queued|building|success|failed
  log_tail    TEXT,                          -- 失敗時の末尾 60 行
  actor       TEXT NOT NULL,
  started_at  TEXT NOT NULL DEFAULT (datetime('now')),
  finished_at TEXT
);

-- 監査ログ(追記のみ・削除する API を作らない)
CREATE TABLE audit (
  id     INTEGER PRIMARY KEY AUTOINCREMENT,
  at     TEXT NOT NULL DEFAULT (datetime('now')),
  actor  TEXT NOT NULL,
  action TEXT NOT NULL,   -- app.create / upload / promote / rollback / suspend …
  app_id TEXT,
  detail TEXT
);
tree_sha を持つのがロールバックの鍵

「あの版に戻す」は、その tree_sha を、親 = 現在の HEAD として新しいコミットを作るだけで済む。履歴を書き換えない。

20GitHub 連携の設計用意する人

GitHub App の権限(過不足なく)

種別権限何に使うか
OrganizationAdministration: Writeリポジトリの作成
OrganizationSecrets: Writeorg secret の対象リポジトリに追加
RepositoryContents: Writeblob/tree/commit/ref。ブランチ作成・マージ
RepositoryWorkflows: Write.github/workflows/** を含む push に必須
RepositoryActions: Read & Writeworkflow_dispatch・run の状態取得・ログ取得
RepositoryVariables: Write資源 ID をリポジトリ変数に入れる
RepositoryMetadata: Read既定(必須)
RepositoryPull requests: Write破壊的変更のときだけ PR を作る

Webhookworkflow_runpush を購読し、/api/webhooks/github で受ける(署名検証必須)。

トークンの流れ

Secrets Store GITHUB_APP_PRIVATE_KEY ① RS256 で JWT に署名(有効期限 10 分) crypto.subtle.importKey('pkcs8', …, 'RSASSA-PKCS1-v1_5') App JWT ② installation token を交換する POST /app/installations/{id}/access_tokens installation token 有効期限 1 時間 ③ KV に 50 分キャッシュして使い回す GitHub API(リポジトリ作成・コミット・dispatch・マージ) キャッシュしない場合 1 回のアップロードごとに署名と token 交換が 走って遅い → KV に 50 分(期限より短く)置く
図 8 — トークンの流れ。KV は結果整合なので、期限切れを掴んだら 1 回だけ再取得する

落とし穴 3 点(実装前に必ず読むこと)

落とし穴 1 — テンプレートからの generate は使えない

POST /repos/{owner}/{repo}/generate は、公式ドキュメント上は installation token に対応していると読めるが、「Resource not accessible by integration」で失敗する報告が多数ある(GitHub Community Discussion #52316)。

対策 = 使わない。空リポジトリを POST /orgs/{org}/repos で作り、テンプレートの中身を Git Data API で初回コミットする。どのみち zip アップロードで同じコードパスを使うので、実装は増えない。

落とし穴 2 — .github/workflows/** は Workflows 権限が無いと push できない

Contents: Write だけでは refusing to allow a GitHub App to create or update workflow で拒否される。Workflows: Write が必要。テンプレートの初回コミットにも保護パスの上書きにも deploy.yml が含まれるので、これが無いと 100% 落ちる。

落とし穴 3 — 誰が push したかで Actions が起動するかが変わる

GitHub Actions 内の GITHUB_TOKEN で push しても、次のワークフローは起動しない(公式ドキュメント明記)。GitHub App の installation token による push は起動する。

アプリハブはポータルから App トークンで push するので問題ないが、「Actions の中から Actions を起こす」実装に逃げないこと。将来 deploy.yml の中で自動コミットを足したくなったときにここで詰まる。

全置換コミットの作り方

1. GET  /repos/{o}/{r}/git/ref/heads/staging          → 現在の commit sha
2. GET  /repos/{o}/{r}/git/commits/{sha}              → 現在の tree sha
3. GET  /repos/{o}/{r}/git/trees/{tree}?recursive=1   → 現在の全ファイル(差分表示用)
4. 採用するファイルごとに POST …/git/blobs           → blob sha(base64 で送る)
   ※ 中身が変わっていないファイルは 3 で得た blob sha を再利用し、POST しない
5. POST …/git/trees   base_tree を付けない ← ★全置換の要
      tree: [{path, mode:"100644", type:"blob", sha}, …]
6. POST …/git/commits  parents:[現在の sha]  tree:新 tree
7. PATCH …/git/refs/heads/staging   sha:新 commit  force:false
要点 3 つ
  • 5 で base_tree を付けないことが「丸ごと置換」の実装そのもの。付けると差分マージになり、消したはずのファイルが残る
  • 4 の blob 再利用が効く。40 ファイルのうち変更は数個なので、API 呼び出しは 10 回前後に収まり、体感が大きく変わる
  • 7 で force:false他の経路で staging が進んでいたら 422 で落ちる。そのときは「他の人が先に上げました。最新を受け取り直してください」と出す。force push は絶対に実装しない

21CI/CD — deploy.yml の設計用意する人

  push (staging)   ──┐
  push (main)      ──┤
  workflow_dispatch──┴─→  1. 環境を決める(ref=main → production / それ以外 staging)
                          2. app.config.json を検査(allowlist)
                          3. package.json と wrangler.jsonc を生成
                          4. npm install
                          5. 構文チェック(tsc --noEmit)      ★
                          6. ビルド(vite build)
                          7. D1 migrations を適用               ★
                          8. seed を適用(未適用のものだけ)
                          9. wrangler deploy
                         10. 疎通確認(/healthz が 200 か)      ★
                         11. 結果をポータルに POST + Chatwork

★ の 3 つがこの基盤の品質を決める

失敗したときの扱い

落ちた場所扱い利用者に出す文言
2(設定の検査)配信しないapp.config.json の◯◯が使えません」+ 許可されている値の一覧
5(構文チェック)配信しないエラー本文の末尾 60 行そのまま +「/service-fix に貼ってください」
7(migrations)配信しない。表は途中まで変わっている可能性がある「表の変更に失敗しました。エンジニアに連絡してください」+ 管理者に通知
9〜10(配信・疎通)自動で 1 つ前に戻す「配信しましたが起動しなかったため、前の版に戻しました」
7 だけ自動復旧しない

DB の巻き戻しは失敗すると被害が大きい。人が見る。

secrets と variables の置き場

置き場中身誰が入れる
org secret CF_API_TOKENCloudflare の API トークン(Workers/D1/R2/KV の Edit)エンジニアが 1 回。対象リポジトリはポータルが自動で足す
org secret APPHUB_WEBHOOK_SECRET結果を POST し返すときの署名鍵同上
repo variable D1_ID ほか資源の ID。秘密ではないポータルが枠作成時に入れる
アプリ固有の秘密(外部 API キー)リポジトリに入れない。Cloudflare Secrets Store に置き、ポータルの画面から登録する管理者

アプリ固有の秘密をつくる人に扱わせないのは原則 2 のとおり。「Chatwork に通知したい」のような要望は、テンプレート側に共通の通知関数を用意して、鍵はプラットフォーム側が持つ形にする。

22資源の自動払い出しと認証用意する人

払い出す API(すべて CF_API_TOKEN で。ポータルから叩く)

資源エンドポイント
D1POST /accounts/{acct}/d1/database {name}
R2POST /accounts/{acct}/r2/buckets {name}
KVPOST /accounts/{acct}/storage/kv/namespaces {title}
Custom DomainPUT /accounts/{acct}/workers/domains {zone_id, hostname, service, environment}
Access アプリPOST /accounts/{acct}/access/apps {name, domain, type:"self_hosted"}
DNS は自分で張らない

DNS レコードは Custom Domain の作成時に Cloudflare が自動で作る。手で *.apps.synon.co.jp のワイルドカードを張る必要はなく、張ると Access の対象がぼやけるので張らないほうがよい。

認証は 3 層

1 層目 — アカウント単位 Access all_workers / all_preview_workers 2026-08-14 GA。新しく作られた Worker が自動で社内限定になる ★ これだけが「誰も気づかないまま   公開されていた」を防ぐ。必ず ON 2 層目 — ホスト名ベースの Access アプリ(アプリごと) mitsumori.apps.synon.co.jp→ ポリシー「営業部」 mitsumori-stg.apps.synon.co.jp→ ポリシー「担当者と承認者のみ」 ★ 見せる範囲を業務単位で決められるのはこの層。ポータルが API で自動作成する 3 層目 — アプリの中での判定 src/worker/lib/access.ts JWT を検証して「誰か」を取り出し、画面や API ごとに出し分ける ★ 保護パスなので、つくる人が壊せない
図 9 — 認証の 3 層。1 層目は「事故の最後の砦」、2 層目が「業務の見せる範囲」、3 層目が「アプリの中の出し分け」
★ Static Assets と Access の既知の制約(重要)

公式ドキュメントに 「Static Assets を有効にした Worker には、ルータが ctx.access を渡さない」と明記されている。テンプレートは Static Assets を使うので、ctx.access は当てにできない。

対策として、2 層目をホスト名ベースの Access アプリにし、アプリ側は Cf-Access-Jwt-Assertion ヘッダを自前で検証するcf-internal-app-starteraccess.ts が既にこの実装)。

ただしヘッダが確実に付くかは実測が必要(→ 28 章 未確認 1)。ここが崩れると設計の前提が変わるので、フェーズ 0 の最初の 1 時間で潰す。

app.config.jsonvisibleTo と Access ポリシーは、ポータルが同期する。つくる人が visibleTo を変えて zip を上げたら、ポータルはその場では反映せず、承認者に確認を出す。「見せる範囲を広げる」は本番公開と同じ重さの操作である。

23zip 受け入れ検査仕様用意する人

アップロードから差分プレビューまでのあいだに、次の検査を順に通す。1 つでも落ちたら受け付けない。

入口(同期・Worker 内)

#検査上限・条件落ちたときの文言
1認証Access の JWT が有効(Access が弾く)
2権限このアプリの担当者本人 or 管理者「このアプリの担当ではありません」
3アプリの状態status = ready「準備中です。少し待ってください」
4サイズ20 MB 以下「大きすぎます。node_modules が入っていませんか」
5形式zip として展開できる「zip として読めません」

中身(Queue コンシューマ内)

#検査条件意図
6ファイル数500 以下暴走の検出
7展開後の合計60 MB 以下zip 爆弾
8圧縮率200 倍以下zip 爆弾
9パス.. を含まない/絶対パスでない/\ を含まないパストラバーサル
10パスUTF-8 として妥当・255 文字以下・制御文字なし破損 zip
11種別シンボリックリンク・特殊ファイルを含まない逃走
12拡張子allowlist(ts tsx js jsx json css html svg png jpg md sql csv txt …)実行ファイルの混入
13除外node_modules/ .git/ dist/ .data/ upload/ を含まない事故の予防(含んでいたら黙って捨てる
14単一ファイル2 MB 以下(画像は 1 MB)素材の置きすぎ
15必須app.config.jsonsrc/worker/index.ts がある中身の取り違え
16app.config.jsonJSON として妥当・スキーマに合う・packages が allowlist 内17 章
17.appmeta.jsonzip の slug と URL の slug が一致★ 別のアプリの zip を上げた事故。これが一番よく起きる
18seeddata/seed/** の行数が 100 行以下本番データの混入
19seedメールアドレス・11 桁の数字列・氏名らしき列を検出したら 保留個人情報の混入
20migrations既存ファイルが変更されていない(新規追加のみ)適用済み migration の書き換えは D1 で不整合になる
21migrationsDROP TABLE / DROP COLUMN / DELETE FROM / TRUNCATE の検出含むならエンジニア承認を必須にフラグ
22差分削除が 10 件超、または data/ の削除があれば 確認チェック必須全消し事故
「拒否」ではなく「保留」にするもの

19(個人情報の疑い)と 21(破壊的 DDL)は拒否しない。拒否すると、つくる人は回避方法を探し始める(ファイルを分割する等)。「管理者に回りました。連絡します」と伝えて人の判断に上げるほうが安全であり、実際に必要な変更なら通る。

第 IV 部 — 運用決める人・管理者

24命名規約と上限管理者

命名(slug から機械的に決まる。例外を作らない)

もの規則例(slug = mitsumori
GitHub リポジトリapp-<slug>synon/app-mitsumori
Worker(本番 / 試用)app-<slug> / -stgapp-mitsumori
ドメイン(本番)<slug>.apps.synon.co.jpmitsumori.apps.synon.co.jp
ドメイン(試用)<slug>-stg.apps.synon.co.jpmitsumori-stg.apps.synon.co.jp
D1app-<slug>-db / -db-stgapp-mitsumori-db
R2app-<slug>-files / -files-stgapp-mitsumori-files

slug の規則: 半角小文字英数とハイフン、3〜20 文字、数字始まり不可、予約語(api admin www apps portal new stg)不可。

上限(ポータルが強制する)

項目理由
アプリ本数(全社)初期 30 本D1 は Paid で 50,000 個なので技術上の制約ではない。面倒を見きれる本数の上限
1 人が担当できる本数5 本担当者不在のアプリを増やさない
zip サイズ / ファイル数20 MB / 50023 章
D1 の容量/アプリ10 GBPaid の上限。超える設計はこの基盤の対象外
R2 の容量/アプリ5 GBポータルが監視・超えたら通知。費用の暴走防止
1 日のアップロード回数/人30 回暴走の検出。超えたら通知だけ(止めない)
本数の上限を最初に決めておく

上限が無いと、半年後に「誰も直せない 60 本」になる。上限に当たったら、新規を作る前に棚卸しで廃止する(工程 8)。

25費用決める人

$13 / 月
合計(アプリ本数によらない)
$5
Workers Paid(アカウント単位)
$8
GitHub Team(エンジニア 2 名)
$0
つくる人 1 人あたり
項目月額備考
Workers Paid$5アカウント単位。アプリ本数によらない。必須
GitHub Team$8$4 × エンジニア 2 名。つくる人はアカウント不要=課金対象外
Cloudflare Access$0現行の料金ページに社内利用の追加課金の記載なし(→ 28 章 未確認 5)
D1 / R2 / Queue$0〜Paid に含まれる枠で社内 30 本なら届かない。R2 は 10 GB まで無料・egress 無料
合計$13アプリ本数によらない

Workers Paid が必須である理由(Free では成立しない)

Free の制約どこで詰まるか
CPU 10 ms/リクエストzip の展開・JWT 検証・D1 アクセスで確実に超える
D1 10 個/アカウント1 アプリで 2 個使うので 5 本で枯渇
D1 容量 500 MB/DB、合計 5 GB台帳 1 本でも窮屈
サブリクエスト 50 回/リクエスト全置換コミットは 40 ファイルで 43 回。足りない
Queues が使えない非同期処理が組めない
稟議を Free 前提で通さないこと

Paid の $5 は「アプリが増えても増えない固定費」として説明するのが通りやすい。

増えるとしたらどこか

26失敗モードと復旧管理者

想定される失敗を、起きる頻度の高い順に並べた。1 週目に必ず出るものから書いてある。

#症状原因復旧予防
1上げたのに「削除 12 件」と出る/service-pack が除外しすぎ/フォルダの取り違え上げない。zip を作り直す差分プレビュー
2別のアプリの zip を上げたCowork のプロジェクトを取り違えた検査 17 で自動的に弾かれる.appmeta.json
3構文エラーで配信が失敗型が合わないエラー本文を /service-fix に貼る/service-pack の事前チェック
4試用では動くのに本番で動かない本番の D1 に表が無い本番の Actions ログを見る。migrations の順序を確認本番でも必ず migrations を先に流す
5「他の人が先に上げました」同じアプリを 2 人が触った最新を受け取り直して作業をやり直す担当者を 1 人に限る
6配信は成功したが画面が真っ白ビルドは通ったが実行時エラー/healthz で検出 → 自動で前の版に戻る21 章 手順 10
7表を消す SQL を書いてしまった「作り直したい」と Claude に言った結果検査 21 で保留 → エンジニア承認data/data.md のルール 3
8枠作成が途中で止まったCloudflare API の一時失敗status=failed。管理者が[撤去]してから作り直す自動ロールバックはしない
9本番のデータが消えたmigrations の破壊的変更が本番に流れたD1 の Time Travel で 30 日以内に復元検査 21 + エンジニア承認
10全アプリの配信が同時に失敗テンプレートの deploy.yml を壊したapphub-template を revert。保護パスは次の配信で自動的に戻るテンプレートの main は PR + 2 名承認

復旧手段の一覧(どこに何が残っているか)

失ったものどこから戻すか保持期間
コードGitHub(全コミット)無期限
上げた zip の原本R2 ARTIFACTS90 日
業務データ(D1)D1 Time Travel30 日
ファイル(R2)世代管理なし。アプリ側で上書き削除しない設計にする
誰が何をしたかD1 audit無期限(追記のみ)
配信ログGitHub Actions / 失敗時の末尾は D190 日 / 無期限
R2 に世代管理が無いのが唯一の穴

「同じ名前で上書きしたら前のファイルが消える」を防ぐため、テンプレートのファイル保存関数は必ずキーに ULID を付ける(保護パスにして触らせない)。

第 V 部 — 導入実装の順番と、やめどきの決め方

27実装の進め方(フェーズ 0〜4)用意する人

原則

全部を作ってから使わせない。各フェーズの終わりに「ここで止めても損しない」状態を作る。

フェーズ 0 前提の実測 半日〜1 日 7 項目を潰す フェーズ 1 手で 1 本通す 2 日 deploy.yml が完成 フェーズ 2 ポータル(読取のみ) 3 日 止めてよい地点 ★ フェーズ 3 配信の自動化 5 日 1 人目に使わせてよい ★ フェーズ 4 枠作成と本番公開 4 日 通しで完成 ★ フェーズ 0 で前提が崩れたら、以降の設計を書き直す。実装より先に必ずやる。 合計 3 週間(エンジニア 1 名・他業務と並行なら 5〜6 週間)
図 10 — 実装のフェーズ。2 と 3 の終わりが「止めても損しない」地点

フェーズ 0 — 前提の実測(半日〜1 日)

見積もりの注意

この 7 項目を潰すのに、たぶん半日ではなく 1 日かかる。見積もりに入れておくこと。

フェーズ 1 — 手動で 1 本通す(2 日)

ポータルを作らない。エンジニアが手で全部やる。

ここで deploy.yml が完成する。これがフェーズ 1 の成果物。ポータルは deploy.yml を起動するだけの存在なので、先にこちらを固める。

フェーズ 2 — ポータルの読み取りだけ(3 日)

ここで止めてもよい

この時点で 「社内 Web サービスの一覧が見られるポータル」としては既に価値がある。

フェーズ 3 — 配信の自動化(5 日)

ここで 1 人目に使わせてよい

「つくる人が自分で試用環境に出せる」状態。フェーズ 4 を待たなくてよい(本番公開だけエンジニアが手で GitHub でマージする)。

フェーズ 4 — 枠の自動作成と本番公開(4 日)

展開の順序

時期やること
1 週目フェーズ 0〜1
2〜3 週目フェーズ 2〜4
4 週目エンジニア自身が 1 本作って通す(つくる人の立場で。ここで PROJECT.md とスキルの文言が固まる)
5 週目意欲の高い 1 名に作らせて横で見る
6〜7 週目同じ 1 名が本番公開まで
8 週目以降部署展開

28未確認事項と撤退基準全員

実機で潰すこと(フェーズ 0 で)

#未確認崩れたときの影響代替案
1Static Assets を使う Worker に Cf-Access-Jwt-Assertion が付くか。公式は「ctx.access は渡らない」とだけ書いており、ヘッダの有無は未記載アプリ内で「誰が使っているか」が分からなくなる。個人別の台帳が作れない① Static Assets を使わず Worker から HTML を返す ② 画面と API を別 Worker に分ける ③ ctx.access が使える構成に戻す
2installation token でのリポジトリ作成が Organization Administration 権限で通るか枠の自動作成ができないエンジニアが手でリポジトリを作り、ポータルは既存リポジトリに紐づけるだけにする(手間 1 分/本)
3generate(テンプレートから生成)の可否 — 使わない設計にしてある影響なし
4Workers のリクエストボディ上限がゾーンプランで決まるという記述の実挙動20 MB 上限には影響しない(Free でも 100 MB)R2 への直接アップロード(presigned URL)に切り替え
5Cloudflare Access の課金。現行の料金ページに社内利用の追加課金の記載が見当たらない人数課金だと費用の前提が変わる既存資料(cloudflare_os_access_control)の訂正事項と整合を取る
6Cowork の接続フォルダで zip を作れるか/service-pack の実装手段)スキルが成立しないCowork のクラウド側で zip を作って会話に添付し、そこからアップロードする
7D1 の Time Travel が migrations 適用後の状態から戻せるかの実挙動失敗モード 9 の復旧手段が無くなる配信前に自動でエクスポート(wrangler d1 export)を R2 に取る手順を deploy.yml に追加

撤退基準(先に決めておく)

やめどき
  • フェーズ 0 の 7 項目のうち 1 が崩れ、代替案 ①〜③ のどれもテンプレートの作り直しになる場合 → この設計を採らず、プラン B のまま Deploy key 方式で進める
  • 5 週目、1 人目が「上げる → 試用で見る」の輪を、補助なしで 3 回まわせない場合 → 展開しない。PROJECT.md とスキルの文言を作り直して、もう 1 名で再試行
  • 「手元で動かして見られない」が耐えられないという反応が 2 人続いた場合 → この方式の中心的な取引が受け入れられていない。プラン B(ローカル開発)に戻すか、Cloudflare OS の「作って試す」を前段に置く併用に切り替える
  • 配信の失敗率が 3 割を超える状態が 2 週間続く場合/service-pack の事前チェックが弱い。ここに投資するか、フェーズ 3 の輪を止める

3 方式 + 本方式の使い分け(既存資料との接続)

既存資料の判定 5 問はそのまま使える。アプリハブはプラン B の置き換えなので、「プラン B」と書いてあるところを読み替える。

Q1 社外の人が使う? Q2 止まると業務が止まる? Q3 個人情報を持ち続ける? Q4 3 か月後も部門で使う? Q5 全部いいえ はいはいはいはい アプリハブ レビューと承認とロールバックがある 社外公開も可(Access を外す判断はエンジニア) まず Cloudflare OS で 30 分の試作 → 定着したら作り直す Cloudflare OS ★ Q4 の「試作 → 作り直す」は変えない。アプリハブでも「作ったが使われなかった」が最大のコストであり、   30 分の試作でそれを潰せる価値は変わらない。
図 11 — 判断フロー。既存資料の 5 問の「プラン B」をアプリハブに読み替える

A付録 — 実装一式(synon-apphub-kit.zip)の中身用意する人

synon-apphub-kit/
├── README.md                       全体の説明と読む順番
├── APPLY.md                        ★導入手順(この順にやれば動く)
├── portal/                         ポータル本体
│   ├── package.json  wrangler.jsonc  tsconfig.json
│   ├── schema/0001_init.sql        19 章の DDL
│   ├── src/index.ts                ルーティング
│   ├── src/types.ts
│   ├── src/lib/access.ts           Cf-Access-Jwt-Assertion の検証
│   ├── src/lib/github.ts           ★App JWT / installation token / 全置換コミット
│   ├── src/lib/cf.ts               D1・R2・KV・Custom Domain・Access の払い出し
│   ├── src/lib/zip.ts              ★展開と 22 項目の検査
│   ├── src/lib/util.ts             ULID・監査ログ・Chatwork・HMAC
│   ├── src/routes/apps.ts          一覧・作成・詳細・ダウンロード
│   ├── src/routes/uploads.ts       アップロード・差分・確定
│   ├── src/routes/deploy.ts        本番公開・ロールバック
│   ├── src/routes/webhooks.ts      Actions からの結果受け
│   ├── src/queue/consumer.ts       ★重い処理の本体
│   └── public/index.html           管理画面(単一 HTML)
├── template/                       テンプレートに足す差分
│   ├── app.config.json  allowlist.json  PROJECT.md
│   ├── data/data.md                つくる人向けの説明
│   ├── data/migrations/0000_apphub.sql
│   ├── scripts/gen-wrangler.mjs    ★app.config.json → wrangler.jsonc
│   ├── scripts/apply-seed.mjs      初期データを 1 回だけ入れる
│   ├── scripts/report.mjs          配信結果をポータルに返す
│   └── .github/workflows/deploy.yml
├── skills/                         Cowork のスキル 4 本
│   ├── service-spec/SKILL.md   service-build/SKILL.md
│   └── service-pack/SKILL.md   service-fix/SKILL.md
└── docs/OPERATIONS.md              運用手順(枠の撤去・秘密の登録・棚卸し)
検証済みの範囲

ポータルの TypeScript は tsc --noEmit が通っている。zip 検査は 9 通りのケース(正常/別アプリ/allowlist 外/破壊的 DDL/個人情報/拡張子/パストラバーサル/必須欠落/差分計算)で動作を確認済み。deploy.yml は YAML として妥当、SQL は SQLite で実行確認済み。実機での結線(GitHub API・Cloudflare API)は未検証なので、フェーズ 0 で潰すこと。

B付録 — 一次情報と社内の関連資料全員

一次情報

社内の関連資料

同じフォルダ /Users/synon/Documents/Synon/プロジェクト/CloudFlare/

資料本書との関係
claude-code-internal-app-workflow-planb.{md,html}プラン B 提案書。本書はこれの配信経路を置き換えたもの
cloudflare-os-internal-service-guide.{md,html,pptx}Cloudflare OS の判断資料。併用の相手
cloudflare-stack-internal-service-stepbystep.{md,html,pptx}作業手順版。工程の分け方を踏襲
cf-internal-app-starter/テンプレートの元。apphub-template はここから作る