気づいたら Cloudflare OS と同じ答えにたどり着いていた
社員全員が Claude Code で業務アプリを本番に出す社内基盤を、Workers for Platforms と自前の API Gateway でエンジニア 1 人で作って 5 ヶ月運用した話。
2026 年 9 月時点。端数は丸めています。会社名・ドメイン・実アプリ名は伏せ、コードは構造を示すための抽象化です。
2026 年 8 月 5 日に Cloudflare が Cloudflare OS を公開しました。社員一人ひとりに AI エージェントの作業場を配り、社内システムへの入口を「Gatekeeper」と呼ぶ Worker で仲介して認証情報をエージェントから隠し、そこで作ったアプリを隔離された Worker として動かす、というオープンソースの基盤です。Cloudflare 社内では 5 月から使われていたとのことでした。読み進めるうちに、弊社で 4 月から動いている社内基盤と構造がかなり重なっていることに気づきました。
規模も動機も違う 2 社が似た形に着地したのなら、それは個社の工夫というより、エージェントと一緒に業務を回すときに避けて通れない部品なのだと思います。この記事では、その部品を弊社がどう組んだかを、できるだけ仕組みの側から書きます。Cloudflare OS との比較は最後に表でまとめます。優劣をつける意図はなく、同じ結論に別の道で着いた者として、どちらがどんな会社に向くかを整理したいだけです。
Cowork から始めて、リポジトリに移った
最初からアプリ基盤を作ろうとしていたわけではありません。全社員に Claude の Team プランのアカウントを配り、Cowork で各自の仕事を速くしてもらう、という普通の導入から始めました。個人の作業は確かに速くなりました。
ただ、業務は一人では完結しません。誰かがエージェント向けに書いた手順書やスキルを、隣の人が今日使いたい、ということがすぐ起きます。個人の環境に閉じたままではそれが会社の資産になりません。そこで Claude Code に切り替え、1 つの Git リポジトリの中に members/{name}/ と teams/{name}/ のディレクトリを切って、各自がそこに文書やスキルを貯める形にしました。このリポジトリを社内では agent-workspace、略して AW と呼んでいます。以下もそう書きます。この時点で「ワークスペース」の意味が、作業場から共有資産の置き場に変わっています。
資産が一箇所に集まると、次の欲が出ます。各自が自分の業務アプリを自由に作れたら、と。ここで欲しかったのは静的サイトのホスティングではなく、隔離されたバックエンド、自分専用の DB、ストレージ、スケジュール実行、そして鍵を自分で管理せずに社外の SaaS へ安全に繋げる口を持った、小さいけれど本物のサーバでした。それが社員の数だけ欲しい。
Cloudflare は Cloudflare OS の記事で、社内知識を集めた「Engineering Codex」への投資が不可欠だったと書いています。順序が同じだったのが印象的でした。文脈の共有が先で、アプリは後。文脈がないままアプリだけ作れるようにすると、何のために何が作られているのか誰も追えなくなります。
最初に決めたこと: エージェントのコードを直接は動かさない
Cloudflare OS は、エージェントが書いたコードを Dynamic Worker としてそのまま実行します。弊社では最初にそれをやらないと決めました。理由は権限の話ではなく透明性です。
10 数人が同じ業務データに触るアプリをそれぞれ作ると、A さんのアプリと B さんのアプリが同じ受注データを別の目的で書き換える、ということが普通に起きます。そのとき「どのアプリが何のデータに何をしているか」を一覧できず、変更の履歴もなく、戻す手段もない状態は、正直なところ怖かった。事故は起きる前提で、起きたときに再現と巻き戻しができる形にしたかった。
Git はそれをただで提供してくれます。バージョン管理と、それ以上にマージ管理です。
運用してみて毎週のように効いているのが調査のしやすさです。「この数字、昨夜から食い違ってない?」と誰かが気づいたとき、関係しそうなアプリのコードもマイグレーションもスケジュール定義も変更履歴も、全部 1 つのリポジトリにあります。Claude Code に「この数字を触りうるアプリを横断して昨夜の変更を探して」と頼めば、そのまま読み始めてくれる。アプリごとにリポジトリが分かれていたり、コードが生成された瞬間に消えていたりすると、調査は「誰が作ったんだっけ」を人に聞くところから始まってしまいます。
問題は非エンジニアに Git を教えることで、これは従来なら骨の折れる仕事でした。ただ Claude Code をリモートで動かすなら、ローカルの環境構築は要らず、Git のコマンドも覚える必要がありません。実際に伝えたのはこの 2 文だけです。
「PR というものを作ると、確認用の URL が出ます。」
「マージというものをすると、本番の URL に反映されます。」
この理解でほぼ全員が今日まで回しています。PR の作成もマージも Claude Code に頼めばやってくれるので、人間がやるのは preview の URL を開いて動きを見ることと、いいと思ったら「マージして」と言うことだけです。
デプロイの経路
基盤は Cloudflare の Workers for Platforms (以下 WfP) の上に組んでいます。本来はテナント別のカスタムコードを動かす SaaS 向けの機能ですが、テナントを社員に置き換えれば要件はそのまま合います。namespace は production と preview の 2 つです。
基盤側で自前に持っている Worker は、リクエストの振り分けをする dispatch Worker と、アプリに DB や cron を貸し出す platform Worker の 2 本。デプロイは GitHub Actions がやります。
デプロイスクリプトは、変更のあったアプリだけを対象に、依存の install とビルド、esbuild でのバンドル、bindings を組んだ WfP scripts API への PUT、cron と登録簿の同期をします。script 名は production が {owner}-{app}、preview が {owner}-{app}-pr{N} で、これがそのままホスト名になります。
アプリは普通の npm プロジェクトである
各アプリは自分の package.json を持つ普通の npm プロジェクトで、Workers で動くものなら何でも入れられます。React と状態管理とチャートライブラリで組んだ管理画面、SQLite へのクエリビルダ、LLM の SDK を叩く分類や要約、外部 SaaS の公式クライアント。「社員が作るミニアプリ」という言葉から想像されるものではなく、エンジニアが作るのと同じ厚みのアプリが、非エンジニアの指示で Claude が書いたコードとして本番で動いています。
これが成り立つのは、コーディングエージェントに触らせているのが「普通のリポジトリ」だからです。対話の中で書いたコードをそのまま動かす方式では、アプリの大きさがモデルのコンテキストに縛られます。リポジトリなら、エージェントは必要なファイルだけ読んで必要な箇所だけ書けばよく、アプリの規模に上限がない。TypeScript も JSX も書けて、コンパイルは GitHub Actions の上で走る。テストもリントもツールチェインとして入れられるので、エージェントが自分の書いたコードを自分で検証してから PR にできる。制約は Workers のランタイム (JS のみ、バイナリ依存不可、CPU 時間の上限) だけで、それは後述するスキルが最初に検査します。
実行時の経路
アプリの env に何が入るかはデプロイ時の metadata で決まり、常に入るのは platform Worker への service binding と自分の名前・環境・URL。api_gateway: true と書いたときだけ API Gateway への binding が足されます。dispatch namespace 自体はアプリに bind していないので、アプリから他のアプリを直接呼ぶ経路は存在しません。図の API Gateway と MCP サーバは AW のリポジトリではなく、事業のメインシステム側のリポジトリにある Worker です。基盤 2 本というのは AW 側の数です。
正直に書くと、この「RPC の引数に worker 名を渡す」設計には隙間があります。ヘルパを通さず RPC を直接叩いて隣の worker 名を渡せば、隣のデータが読める。WfP には呼び出し元の script 名を呼ばれた側で検証する手段が無く、いまはヘルパ経由を規約にしてレビュー時に弾いています。
アプリ側が書くコードは、実質これだけです。
// worker.ts (アプリの作者が書くのはこのファイルと config.json)import { defineWorker, createSQL, createApiGateway } from "@company/platform";import m0001 from "./store/migrations/0001_init.sql";
export default defineWorker({ async fetch(req, env) { const sql = createSQL(env, { migrations: [m0001] }); // 自分の DO の SQLite const gw = createApiGateway(env); // 鍵は持たない。URL を投げるだけ const res = await gw.fetch("https://api.some-saas.com/v1/orders?since=2026-09-01", { reason: "日次の受注集計", // write なら必須。監査ログに残る }); // ... 集計して sql に書く }, schedules: { daily: async (env) => { /* cron。誰として動くかは platform が決める */ }, },});cron はどう動いているか
WfP の namespace に置いた Worker には通常の cron トリガを設定できないので、cron は platform 側の DO で作っています。
一つ落とし穴がありました。DO の alarm ハンドラは throw するとストレージ書き込みごと巻き戻してリトライするので、アプリ側の失敗をそのまま throw すると次の alarm をセットした書き込みも巻き戻り、リトライが尽きた時点でスケジュールが永久に死にます。実行失敗は握ってログに残し、次の alarm だけは必ず張る形にしています。
Gatekeeper に相当するもの: API Gateway
Cloudflare OS の Gatekeeper に当たるのが、弊社では API Gateway (以下 GW) 1 本です。これは AW のために作ったものではありません。事業のメインシステム (EC 周辺の API サーバ) が外部 API を叩くための基盤として先にあり、それを MCP で Claude から使えるようにする過程で「誰が」「何を」「なぜ」を記録する必要が出て、権限管理と監査が育ちました。その結果、社員のアプリ基盤にそのまま噛み合った。なので今、事業の本番コード、MCP 経由の Claude、社員が作ったアプリの 3 者が同じ 1 つの権限表を通っています。
route の定義は 1 つの配列で、外部 API が 40 を超えた今もこの形です。認証方式 (静的キー、OAuth、Google の Domain-wide Delegation) は route ごとに違いますが、アプリから見える I/F は全部同じ「URL を投げる」です。
{ permissionId: "shop_admin", match: (url) => url.hostname === "example.myshopify.com", authenticate: (req, secrets) => withHeader(req, "X-Access-Token", secrets.SHOP_TOKEN), resolveCategory: ({ method, body }) => method === "POST" && /^\s*mutation\b/.test(body.query) ? "write" : "read", defaultPermission: { read: "allow", write: "deny" }, requireReason: true,}write と、requireReason を付けた route では理由のヘッダが必須です。理由は自由文で、アプリのコードやエージェントが書きます。監査ログは read も含む全リクエストに「誰が・何のために・どの API を・read か write か・結果どうなったか」が付くので、後から追うときに想像以上に楽です。
「誰が呼んだか」を偽装できなくする
GW の権限主体は「呼び出し元の人の email」です。その email はヘッダで運んでいるので、アプリがそれを偽装できないことがこの基盤の要になります。守っている不変条件は 2 つです。
1 つ目。内部専用ヘッダは、外から来たら必ず剥がす。個別のヘッダ名を列挙する方式にしなかったのは、新しい内部ヘッダを足したときに削除リストの更新を忘れると、そのヘッダだけ偽装できる穴が開くからです。Zero Trust が付ける本人確認のヘッダは、Access のエッジが受信ヘッダを上書きする仕様なので外から詐称できません。
2 つ目。identity の抽出は platform 層の入口だけがやる。アプリ間呼び出しの caller → MCP 経由 → Zero Trust の優先順で 1 つ選んで AsyncLocalStorage に載せ、GW を叩くヘルパはそこから読みます。アプリのコードには email を渡す引数が存在しないので、正直なアプリが間違って別人として叩くことはできません。
// dispatch: 外から来たリクエストからは、内部専用のプレフィックスを持つヘッダを全部落とすfor (const [name] of req.headers) { if (name.startsWith("x-aw-") || name.startsWith("x-gw-")) headers.delete(name);}
// platform (defineWorker の入口): identity はここでだけ決まるconst email = req.headers.get("x-aw-caller-user") // アプリ間呼び出し (platform が刻む) ?? req.headers.get("x-gw-user-email") // MCP 経由 (MCP サーバが刻む) ?? req.headers.get("cf-access-authenticated-user-email"); // Zero Trust (Access edge が刻む)const isSystem = req.headers.has("x-aw-caller-token"); // 社外トークン経由の印 (後述)return runWithIdentity({ email, isSystem }, () => app.fetch(req, env));宣言した分だけ世界が開く
{ "title": "受注集計", "description": "受注を日次で集計して一覧にする", // この 2 行がアプリの責務の定義 "sql": true, "api_gateway": true, "schedules": [{ "id": "daily", "cron": "0 22 * * *" }], "mcp": { "exposed": true }, "callable_by_apps": ["team-x-dashboard"], "api": { "public": true }}登録簿 (AppRegistry DO) は production 固定の 1 インスタンスで、preview デプロイでは何も登録しません。preview のアプリは MCP からも他アプリからも呼べません。一方で preview のアプリが他アプリを呼ぶときの相手は production です。PR で動作確認するときに隣のアプリの本物のデータを読めないと確認にならないためで、preview で既定を閉じているのは cron だけです。
Cloudflare の記事で共感した箇所がもう一つあります。社内で「vibe coded apps」が氾濫し、パターンが見えてくるまで整理に時間がかかったという話です。弊社はこれを「1 アプリ = 1 目的」というルールと、上の title / description で塞いでいます。そこで説明できない機能はそのアプリに足せない。「認証情報や似たコードがあのアプリにあるから」は相乗りの理由にならず、別アプリとして切り出して連携させます。「一旦ここに置いて後で切り出す」は選択肢にありません。運用が始まったコードを切り出す動機は、だいたい失われるので。
人間とシステムを混ぜない
ドメイン全体が Zero Trust 配下なので、ブラウザでログインする人間しかアプリに届きません。裏返すと、社内バッチや外部 SaaS のような、対話しないシステムからは呼べない。そこで 1 ホストだけ Zero Trust を外し、管理者が発行する静的トークンで守る入口を切りました。
トークン 1 本は 1 アプリを指し、記録は発行者・振る舞う identity (サービスアカウント風の email で、これが GW の権限主体になる)・利用者の 3 者を分けて持ちます。一番気を使ったのは、それまでのアプリが「email が来る = ログインした人間」という前提で権限を書いていたことです。トークン基盤を足した瞬間にその前提が壊れるので、トークン経由には「システム」の印を刻み、アプリが requireHuman() / requireSystem() で分岐できるようにしました。判定材料が無いときはどちらも拒否側に倒します。さらにトークン経由では GW を通した外部 API への outbound を既定で拒否し、必要なトークンにだけ発行時に許可を付けます。トークン 1 本の漏洩が、外部 SaaS への破壊的な書き込みに直結しないように。
エージェント側の文脈はスキルで持つ
Cloudflare が「Engineering Codex」と呼んだものは、弊社ではリポジトリ直下の CLAUDE.md と、いくつかのスキルです。
- 依頼を噛み砕くスキル。 雑な依頼や他ツール由来の設計書を、ステップ、MUST / SHOULD、成功条件、そして「アプリで自動化する / 都度エージェントに頼む / やらない」の 3 択に落とします。アプリを作らないという結論を出せるのが大事で、これが無いと全部アプリになります
- アプリを作るスキル。 基盤の制約 (JS のみ、CPU 時間の上限、Worker はステートレス、バイナリ依存不可) を先に検査し、外れていれば設計を直します。「月末に 10 万件まとめて処理」は「毎日 3,000 件ずつ、進捗を SQL に置いて続きから」に崩す、といった翻訳もここでやります。置き場所が個人かチームか曖昧なら必ず人に聞きます。URL が変わるので、後から動かすコストが高い
- 個人スキルの opt-in ロード。 自分専用のスキルは自分のディレクトリに置き、環境変数で指定した人のセッションにだけ読み込まれます
スキルはコードと同じリポジトリにあり、同じ PR で育ちます。基盤の制約が変わればスキルが変わり、次に誰かがアプリを作るときにはもう反映されている。文脈をコードと別の場所に置かなかったことは、振り返るといちばん効いた判断でした。
つながると業務の部品になる
ここまでの仕組みが揃うと、個人の効率化のために作ったアプリが他の人の業務の部品になります。抽象化した例を 1 つ。
運用は私 1 人で、日常の手作業はほぼありません。メンバーの追加は owners ファイルに 1 行、preview の掃除は自動、トークンの発行は Zero Trust 配下の小さな管理画面から。基盤側の変更も同じリポジトリの PR で入り、同じ CI がデプロイします。
Cloudflare OS と並べる
同じ結論に着地した部分と、選択が分かれた部分を並べます。分かれた部分は、どちらが正しいかではなく、どんな会社に向くかで読んでもらえると。
| 観点 | Cloudflare OS | 弊社の基盤 | 読み方 |
|---|---|---|---|
| 鍵の隔離 | サービス別 Gatekeeper、型付き binding | 1 本の GW、URL ベースで注入 | 同じ結論。アプリは鍵を見ない |
| 権限の主体 | エージェント × Gatekeeper のポリシー。ゼロから付与 | 操作している人の email × read / write。write は理由必須 | 同じ結論。「本人以上の権限を持たない」 |
| アプリの隔離 | Dynamic Worker + DO Facet、個別 SQLite | Workers for Platforms + DO、個別 SQLite | 同じ結論 |
| 文脈の共有 | Engineering Codex、スキル | リポジトリの CLAUDE.md とスキル | 同じ結論。文脈が先、アプリが後 |
| コードの実行 | エージェントが書いたものを直接実行 | Git を挟む。PR で preview、マージで本番 | 違う選択。 各自の業務が密接に連携し、同じデータに複数のアプリが触る会社では、履歴とマージ管理を土台にした方が任せられる範囲が広い。独立したツールを大量に速く生やす場面では直接実行が向く |
| 外部ネットワーク | アプリから遮断。capability 経由のみ | 遮断していない | 違う選択。 outbound を絞るのは技術的にはすぐできる。していないのは、作る動機が「業務システムに繋ぎたい」なので GW を通さない fetch に旨みがなく、縛りのデメリットの方が大きかったから |
| 汚染追跡 | 読んだものに応じて後続の送信や共有を制限 | 持たない | 違う選択。 PR を挟んでいて、10 数人なら教育とルールで足りる規模。必要になれば足せる場所は用意してある |
| 実行環境 | 自前の一時的な環境 | Claude Code に委ねる | ハーネス非依存で軽い。環境の統制は外に依存する。両面ある |
| 事業システムとの関係 | 別基盤 | 本番コードと同じ GW、同じ権限表 | 弊社の構成のいちばんの利点。ただし GW を先に持っていた偶然による |
並べてみて思うのは、上 4 行がそっくりなことです。会社の規模も、作った人の数も、動機も違う。それでも「鍵をアプリに渡さない」「権限が人を超えない」「アプリを隔離する」「文脈を先に共有する」は同じ形になりました。
GW を先に持っていない会社なら、既存の本番コードの外部 API 呼び出しを GW に寄せるところから始めるのがいいと思います。AW が無くても単独で価値があります。鍵が 1 箇所に集まり、誰が何のために何を叩いたかのログが残る。
おわりに
これから始める会社は、Cloudflare OS を入れるのがいちばん速いと思います。一方で、社員同士の業務が密に絡み、同じデータに複数の手が触るなら、コードとバージョンの透明性を土台にした方が任せられる範囲が広がります。弊社はそちらを選んで、5 ヶ月で 50 を超えるアプリと 1,500 を超える PR になりました。運用しているのは相変わらず 1 人です。どちらの道を選んでも、この部品が揃った後の業務の進み方は、揃う前とは別物になります。
参考: Cloudflare OS: an open platform for agents, apps, and work / How we’re rethinking work at Cloudflare with Cloudflare OS / cloudflare/cloudflare-os
本文中のコードは構造を示すための抽象化で、実際のヘッダ名・ドメイン・アプリ名とは異なります。数字は丸めています。
公開 2026年9月3日