Skip to content

Latest commit

 

History

History
563 lines (438 loc) · 46.6 KB

deploy.md

File metadata and controls

563 lines (438 loc) · 46.6 KB

デプロイ

CANSHI の Cloudflare 環境へのデプロイ手順・運用設定をまとめる。docs/plans/ の実装計画には含めず、本ファイルに集約する。

前提

  • ホスティング:Cloudflare Workers
  • ビルド:@opennextjs/cloudflare で next build の出力を Workers 向けに変換する
  • DB:Cloudflare D1
  • オブジェクトストレージ:Cloudflare R2(写真・動画の本体は非公開バケットに保存)
  • バックグラウンド処理:Cloudflare Workflows(定期通知のスケジュール実行)
  • 外部アクセス制御:Cloudflare Access(プレビュー URL と管理画面。メインアプリの本番ドメインは Access で保護せず、アプリのログインで利用者を制限する)
  • アプリ内のログイン・認可:Cloudflare Access の SaaS OIDC アプリでログインし、D1 に保存する ログインセッション(Cookie にはセッショントークンだけを保存)で認可する。 ユーザーは所属する「家」の猫だけを参照できる。メールアドレス・パスワードでのログインは持たない

必要な Cloudflare リソース

  • Workers(アプリ本体)
  • Workers(管理画面、packages/admin)と、管理者だけを許可する Access の Self-hosted アプリケーション
  • D1 データベース(本番・ステージング用に分離するかは要検討)
  • R2 バケット
    • メディア(写真・動画)用バケット(非公開)
    • ISR・SSG のキャッシュ用バケット(メディア用とは分離する。docs/idea/index.md の方針)
  • Cloudflare Workers 全体の Access 設定と許可ポリシー(プレビュー URL の保護に使う)
  • Cloudflare Workflows(定期通知が実装される第2段階以降で必要)

環境変数・シークレット

docs/plans/01_project_setup.md で用意する .env.example を基準に、本番用の値を Cloudflare のシークレット管理(wrangler secret put 等)で設定する。

  • D1・R2 のバインディング名(wrangler.toml で定義)
  • ログイン(OIDC)の設定と、メールアドレスをハッシュにする SALT(「ログインと家」節の 「ログイン(Cloudflare Access の OIDC)」を参照)
  • OpenAI API キー(第3段階の AI 機能実装時に追加)

デプロイ手順

  • メインアプリは、Cloudflare 側の GitHub App(Workers Builds)で自動的にデプロイされる。 main にマージされるとデプロイがトリガーされるため、手動で wrangler deploy する必要はない。 GitHub Actions からはデプロイしない
  • 管理画面(packages/admin)も同じく Workers Builds で、管理画面に関わるファイルが変わったときだけ 自動でデプロイされる(「管理画面の自動デプロイとプレビュー」節)
  • 本番 D1 のマイグレーションは自動では適用されない(Workers Builds のブランチのプレビューも適用しない)。 pnpm db:migrate:remote(wrangler d1 migrations apply DB --remote)を手動で実行したときだけ適用される
    • マージと同時に新しいコードがデプロイされるため、新しい列・テーブルを使うコードを含む PR では、 マイグレーションの適用とマージの順序に注意する。列・テーブルを追加するだけのマイグレーションは、 マージ前に適用しても旧バージョンのアプリはそのまま動く
    • 適用のタイミングは PR 本文に書く

未着手の検討事項:

  • ステージング環境を用意するかどうか

プレビュー(Worker Previews)

main 以外のブランチは、Workers Builds が Worker Previews (wrangler preview、2026-11 時点でオープンベータ)でプレビューを作る。プレビューの URL は <ブランチ名>-canshi.<サブドメイン>.workers.dev で、ブランチに push するたびに更新される。

プレビューは本番の設定(vars・バインディング・シークレット)を引き継がない。使うものは wrangler.toml の [previews] に書き直している。

  • D1 はプレビュー専用の canshi-preview-db、R2 はプレビュー専用の canshi-media-preview を使う。 本番のデータには触れない。すべてのブランチのプレビューで同じ DB・バケットを共有する
  • DANGEROUSLY_ENABLE_DEVELOPMENT_LOGIN = "true"([previews.vars])で、Cloudflare Access(OIDC)を 通さない開発用のログイン(メールアドレスを入力するだけでログインする)を有効にする。 EMAIL_HASH_SALT が未設定なら開発用の SALT を使う(src/features/auth/infrastructure/developmentLogin.ts)
    • このフラグは [previews.vars] にだけ書き、トップレベルの [vars] やシークレットには決して設定しない。 本番のドメインは Access で保護していないため、本番で有効になると誰でも任意のメールアドレスで ログインできてしまう
    • プレビューの URL は Workers 全体の Access で保護する(「Cloudflare Access 設定」節)。 Access を通れる人は、プレビューでは任意のメールアドレスで(つまり誰としてでも)ログインできる
  • Cron Triggers はプレビューでは動かない(scheduled が呼ばれない)ため、定期通知の生成・Push の送信・ 期限切れの OAuth の行の削除は行われない。Workflow(NOTIFICATION_WORKFLOW)は本番の Workflow を 共有することになるため、バインドしていない
  • シークレット(VAPID 鍵・OIDC の設定)は設定していない。Push の送信は自動的にスキップされる
  • assets・compatibility_*・observability はトップレベルの値がプレビューにも使われる

プレビュー用 D1 のマイグレーション

プレビュー用 D1 のマイグレーションも自動では適用されない。マイグレーションを含むブランチの プレビューを確認するときは、手動で適用する。

pnpm db:migrate:preview

wrangler d1 migrations apply はトップレベルの d1_databases しか見ないため、 wrangler.preview-migrations.toml に同じ DB を書いて --config で指定している。 wrangler.toml の [[previews.d1_databases]] を変えるときは、このファイルも揃える。

すべてのブランチで同じ DB を共有するため、まだマージしていないマイグレーションを適用すると、 ほかのブランチのプレビューにも影響する。作り直したマイグレーションを適用し直したいときは、 プレビュー用 D1 を作り直してよい(本番のデータは入っていない)。

確認用のデータが要るときは、VRT のシード(e2e/fixtures/vrt-seed.sql)を入れると、 e2e@example.com でログインして VRT と同じ猫・記録を見られる(開発用の SALT が前提)。

pnpm exec wrangler d1 execute DB --remote --config wrangler.preview-migrations.toml --file=e2e/fixtures/vrt-seed.sql

初回の切り替え手順

既存の Worker は、ダッシュボードで一度切り替えるまで以前の方式(wrangler versions upload)で プレビューを作る。切り替えは元に戻せない。

  1. この設定([previews]・preview_urls = true)を含む変更を main にマージし、本番にデプロイする。 preview_urls は wrangler deploy でだけ反映される。
  2. Workers & Pages → canshi のビルド(Workers Builds)の設定で、Worker Previews への一度だけの切り替えを行い、main 以外のブランチの デプロイコマンドを npx wrangler preview --ignore-base-config にする(ビルドコマンドは今のまま、OpenNext のビルドを使う)。 Worker Previews ではブランチごとに設定するため、失敗するブランチがあればそのブランチの設定を確かめる。
    • ダッシュボードの Previews Base(プレビューのひな形)に本番の設定をインポートしない。wrangler preview は 既定で Previews Base の設定も合わせて使うため、本番の D1・R2・Workflow が入るとプレビューから本番のデータに 触れてしまう。--ignore-base-config を付け、wrangler.toml の [previews] だけを使う (シークレットも Previews Base に置かないため、プレビューには何も設定されない)
  3. Domains → Worker URL で Preview が有効になっていることを確かめる。
  4. 最初のプレビューができたら、ログインしていないブラウザ(シークレットウィンドウなど)でプレビューの URL を開き、Access のログイン画面が出ることを確かめる。 出ない場合は、開発用のログインで誰でも 入れてしまうため、すぐに DANGEROUSLY_ENABLE_DEVELOPMENT_LOGIN を外す(またはプレビューを消す)。
  5. 最初のプレビューのビルドのログで、バインディングが次のとおりになっていることを確かめる。
    • DB が canshi-preview-db、MEDIA_BUCKET が canshi-media-preview を指している
    • DANGEROUSLY_ENABLE_DEVELOPMENT_LOGIN がある
    • NOTIFICATION_WORKFLOW がない
  6. Access を通ったあと、/login に「メールアドレス(開発用)」の欄が出て、ログインできることを確かめる。

ローカルから wrangler preview を実行するときは、先に pnpm cf:build で OpenNext のビルドを作る。 プレビューの名前は今のブランチ名になる(--name で変えられる)。

ログインと家(ユーザー・猫の紐付け)

ログインセッションは D1 の sessions テーブルで管理する。Cookie(canshi-session)にはランダムな セッショントークンだけを保存し、DB にはそのハッシュを保存する(src/features/auth/infrastructure/sessions.ts)。 src/middleware.ts はリクエストごとに Cookie のトークンを D1 のセッションと照合し、有効なセッションが なければ /login へリダイレクトする(API・メディアは 401)。 猫は「家」に所属し、ユーザーは所属する家の猫だけを参照できる(テーブル設計は src/db/README.md を参照)。

ログイン(Cloudflare Access の OIDC)

/login のログインボタンは /login/start から Cloudflare Access の SaaS OIDC アプリへ リダイレクトし、Access でログインすると /login/callback に戻る(認可コードフロー + PKCE、 src/features/auth/infrastructure/oidc.ts)。メールアドレス・パスワードでのログインは持たない。

  • 要求するスコープは openid email だけで、ID トークンからはメールアドレスだけを使う
  • DB にはメールアドレスそのものを保存しない。SALT(EMAIL_HASH_SALT)を使って argon2id で ハッシュにした値を users.email_hash に保存し、同じユーザーかどうかもこの値で判定する (src/features/auth/domain/emailHash.ts)。ハッシュの計算に CPU を 200 ms 以上使うため、計算は ログイン時だけ行う(Workers の Paid プランが前提)
  • 初めてログインしたメールアドレスでは、ユーザーを新しく作る(家には所属しない。家への招待 URL で 参加する)。ユーザーが 1 件もないときに最初にログインしたユーザーは管理者になる
  • EMAIL_HASH_SALT は運用を始めたら変えない。 変えると全員のハッシュが一致しなくなり、 ログインすると別の新しいユーザーになる。ハッシュのパラメータ(EMAIL_HASH_PARAMS)も同じ

設定する値(いずれも wrangler secret put で設定する。ACCESS_DISCOVERY_URL は機密情報では ないが、wrangler.toml の [vars] にない値はデプロイのたびに消えるため、シークレットにする):

名前 内容
ACCESS_DISCOVERY_URL SaaS OIDC アプリの OIDC discovery URL(.../.well-known/openid-configuration)
ACCESS_CLIENT_ID SaaS OIDC アプリの Client ID
ACCESS_CLIENT_SECRET SaaS OIDC アプリの Client secret
EMAIL_HASH_SALT メールアドレスのハッシュに使う SALT(16 文字以上。openssl rand -hex 32 などで作る)
wrangler secret put ACCESS_DISCOVERY_URL
wrangler secret put ACCESS_CLIENT_ID
wrangler secret put ACCESS_CLIENT_SECRET
wrangler secret put EMAIL_HASH_SALT

ACCESS_* のどれかが未設定の本番環境では、ログインボタンを押すとエラーになる(ログインせずに 使える状態にはならない)。

SaaS OIDC アプリの作成:

  1. Zero Trust の Access controls > Applications で SaaS アプリケーションを追加し、 認証プロトコルに OIDC を選ぶ。
  2. Redirect URL に https://canshi.cateiru.dev/login/callback を登録する。スコープは openid と email だけにする。PKCE を有効にする。
  3. メインアプリの利用者を許可するポリシーを付ける(Workers 全体の Access と同じポリシーでよい)。 メインアプリの本番ドメインは Access で保護していないため、このポリシーが「誰が CANSHI を 使えるか」を決める(「Cloudflare Access 設定」節)。
  4. 表示された Client ID・Client secret・OIDC discovery URL を、上の手順でシークレットに設定する。

Redirect URL は登録したホスト名でしか使えないため、Workers Builds のプレビュー URL では OIDC でログインできない。プレビューでは代わりに開発用のログインを使う(「プレビュー(Worker Previews)」節)。

ローカル開発(next dev)では、ACCESS_* が未設定なら /login が開発用のログイン(Access を 通さず、入力したメールアドレスでログインする)になる。e2e・VRT もこれを使う。EMAIL_HASH_SALT が 未設定なら開発用の SALT を使う(VRT の seed はこの SALT を前提にしている)。.dev.vars に ACCESS_* を設定すれば、ローカルでも Access でのログインを確認できる(Redirect URL に http://localhost:3000/login/callback を追加する)。開発用のログインは本番ビルドでは使えない (src/features/auth/infrastructure/developmentLogin.ts)。

OIDC でのログインの導入手順

OIDC でのログインの導入前(マイグレーション 0041 より前)から使っていたユーザーは、メールアドレスの ハッシュを持たない。紐付けずにログインすると、同じ人でも別の新しいユーザー(家に未所属)が作られる。 また、導入前のセッションはすべてログインボタンで作られた同じユーザーのものなので、導入後に削除する。 次の順で導入する。

  1. 上の手順で SaaS OIDC アプリを作り、4 つのシークレットを設定する。

  2. pnpm db:migrate:remote で 0041 を適用する。列とインデックスを追加するだけなので、デプロイより 前に適用しても旧バージョンのアプリはそのまま動く。

  3. 既存のユーザーにメールアドレスを紐付ける。ユーザーの ID は管理画面のユーザー一覧で確かめる。 SALT は手順 1 で設定した値と同じにする(違うとログインしても一致しない)。

     read -rs EMAIL_HASH_SALT && export EMAIL_HASH_SALT
     pnpm user:link-email --remote --user-id <ユーザー ID> --email <Access でログインするメールアドレス>
    
  4. マージしてデプロイする。

  5. 導入前のセッションをすべて削除する。全員がログアウトし、次のログインから Access でログインする。

     pnpm exec wrangler d1 execute DB --remote --command "DELETE FROM sessions"
    

手順 3 より先にログインしてしまうと、そのメールアドレスで新しいユーザー(家に未所属)が作られ、 手順 3 のスクリプトは「すでに別のユーザーに紐付いています」で止まる。その場合は、作られたユーザーの セッションと行を削除してから手順 3 をやり直す(新しいユーザーは家に所属しておらず、記録も持たない)。

pnpm exec wrangler d1 execute DB --remote --command "DELETE FROM sessions WHERE user_id = '<新しいユーザーの ID>'; DELETE FROM users WHERE id = '<新しいユーザーの ID>'"

セッションの有効期限とログアウト

  • セッションの有効期限はログインから 30 日で、延長しない。期限が切れたら再度ログインする
  • ログアウトするとセッションを D1 から削除するため、ログアウト前の Cookie を使い回してもログインできない
  • 利用者は設定の「アカウント設定」から、自分のセッションをすべて削除できる(すべての端末からログアウト)
  • 利用者は「アカウント設定」の「ログイン中の端末」で、ログインしている端末(ログイン時の User-Agent から 変換した端末名とログイン日時)を確認し、端末を指定してログアウトさせられる
  • 特定のユーザーを強制的にログアウトさせるときは、管理画面のユーザー一覧の「すべての端末からログアウト」を 使う(sessions テーブルからそのユーザーの行をすべて削除する)
  • セッションを削除しても、MCP コネクタ(ChatGPT など)の接続は解除されない。MCP サーバーの OAuth の 認可はセッションとは別に(oauth_kv_entries テーブル)保存している(「MCP サーバー」節)

導入時の手順(既存の猫の紐付け)

家の導入(マイグレーション 0036)より前から登録されていた猫は、どの家にも所属しないため アプリに表示されない。マイグレーションでユーザー・家を自動で作らないため、次の順で導入し、 デプロイ後に既存の猫を紐付ける。

  1. pnpm db:migrate:remote でマイグレーションを適用し、メインアプリをデプロイする。マイグレーションは テーブルと列を追加するだけなので、先に適用しても旧バージョンのアプリはそのまま動く。逆に マイグレーションより先にデプロイすると、sessions テーブルがないためログインできない。
  2. /login を開いてログインする。ユーザーが 1 件もなければ、管理者ユーザーが作られる。
  3. pnpm household:link --remote を実行する。最初に登録されたユーザー(--user-id または --email で指定もできる)をオーナーとする家を作り (名前は --name で指定、既定は「わが家」)、家に未所属の猫をすべてその家に紐付ける。 ユーザーがすでに家に所属している場合は、その家に紐付ける。家に未所属の支出記録(マイグレーション 0039 で家を決められなかったもの)と、家に未所属のごはん商品・ごはんプリセット(マイグレーション 0040 より前に登録したもの)も同じ家に紐付ける。ユーザーに紐付いていない Web Push の 購読(導入前に登録したもの)も、同じユーザーに紐付ける。何度実行しても結果は変わらない。

ローカル開発の DB では pnpm household:link --local --persist-to ./.wrangler/state を実行する。

  • 猫を新しく登録すると、ログイン中のユーザーの家に所属する。家に所属していないユーザーは猫を 登録できない
  • MCP のトークンには、同意したときにログインしていた CANSHI のユーザー ID を保存する (「MCP サーバー」節)

R2 バケット(メディア)

写真・動画の本体とサムネイルは wrangler.toml の [[r2_buckets]](バインディング名 MEDIA_BUCKET、バケット名 canshi-media)に保存する。ローカル開発では Miniflare が .wrangler/state 配下にエミュレートするため、バケットの作成は不要。

本番バケットの作成

wrangler r2 bucket create canshi-media
  • バケットは非公開のまま運用する(パブリックアクセス・カスタムドメインは設定しない)。配信はアプリの GET /media/[assetId] 経由で行い、ログインセッションで保護する(猫に紐付くメディアは、その猫の家のユーザーだけが取得できる)
  • オブジェクトキーは {recordType}/{recordId}/{assetId}(元データ)と {recordType}/{recordId}/{assetId}.thumb.webp(サムネイル)

上限値

1 ファイルあたりの上限と合計の保存容量は環境変数で上書きできる(既定値は画像 10 MB/動画 100 MB/合計 10 GB)。wrangler.toml の [vars] またはダッシュボードで設定する。

[vars]
MEDIA_MAX_IMAGE_BYTES = "10485760"
MEDIA_MAX_VIDEO_BYTES = "104857600"
MEDIA_STORAGE_LIMIT_BYTES = "10737418240"
  • 合計容量は media_assets.size_bytes と thumbnail_size_bytes の合計で判定する。R2 の実使用量とは独立した値のため、R2 側のオブジェクトを直接操作した場合はずれる
  • Workers のメモリ上限(128 MB)の都合上、動画の上限を 100 MB より大きくすることは推奨しない。アップロード時にリクエスト本体をメモリに展開するため、大きすぎる値はメモリ不足で失敗する
  • サムネイル生成(WASM)は CPU 時間を消費する。Workers Free プランの CPU 時間上限(10 ms/リクエスト)では処理が失敗しうるため、Paid プランを前提とする
  • Workers はメモリ上限(128 MB)の都合上、元画像をデコードしない。ブラウザが長辺 1024px に縮小したサムネイル候補だけをデコードし、候補がない場合は 4 MP(MAX_DECODE_PIXELS)以下の画像に限って Workers 側でサムネイルを生成する。それより大きい画像は「画像が大きすぎるためサムネイルを作成できませんでした」として拒否する

容量の監視

  • ダッシュボードの R2 ページでバケットの使用量を確認する。R2 の無料枠(10 GB/月)を超えないよう、MEDIA_STORAGE_LIMIT_BYTES は無料枠以下に保つ
  • 記録・猫の削除時にアプリが R2 のオブジェクトも削除するが、R2 の削除だけ失敗した場合は media_assets 行が残る(再度削除すれば片付く)。行の削除後に R2 側だけ残る孤立オブジェクトは発生しない設計だが、念のため定期的に wrangler r2 object list 等で確認する

Cloudflare Access 設定

Cloudflare ダッシュボードの Workers & Pages で、Workers 全体を保護する設定を All traffic として設定済み(2026-09 時点)。この設定により、既存・新規 Worker の本番 URL とプレビュー URL へのリクエストは、Worker が実行される前に Access で評価される。

メインアプリ(Worker 名 canshi)は、Worker 単位の設定で本番のカスタムドメイン(canshi.cateiru.dev)だけを Access の対象から外している(2026-10 時点)。本番ドメインでの利用者の制限は、アプリのログイン(Access の SaaS OIDC アプリ。「ログインと家」節を参照)で行う。

  • src/middleware.ts が、src/features/auth/domain/publicPaths.ts に挙げたパス(/login、PWA の manifest・アイコンなど)以外のすべてのリクエストで、D1 のログインセッションを確認する
  • ログインできるのは、SaaS OIDC アプリのポリシーで許可された人だけになる。初めてログインしたメールアドレスではユーザーが作られるため、このポリシーが「誰が CANSHI を使えるか」を決める
  • プレビュー URL は、引き続き Workers 全体の Access で保護する。プレビューは本番とは別の D1・R2 を使うが、開発用のログイン(DANGEROUSLY_ENABLE_DEVELOPMENT_LOGIN)で誰としてでもログインできるため、Access で利用者を絞る必要がある(「プレビュー(Worker Previews)」節)
  • workers.dev は無効にしている

アプリケーション内で Cf-Access-Jwt-Assertion を検証する処理や、そのための Team ドメイン・Audience タグの環境変数は持たない。

設定の確認

  1. Cloudflare ダッシュボードの Workers & Pages を開く。
  2. Overview の Protect all Workers で、保護対象が All traffic になっていることを確認する。
  3. Workers 全体の Access に適用されているポリシーが、意図したユーザーまたはグループだけを許可していることを確認する。
  4. Workers & Pages から CANSHI の Worker を開き、Access の対象から外しているのが本番のカスタムドメインだけであること、プレビュー URL が保護されていること、workers.dev が無効であることを確認する。
  5. Zero Trust の Access controls > Applications で、CANSHI のホスト名・パスに対する、意図しない公開設定や Bypass ポリシーがないことを確認する。
  6. メインアプリの SaaS OIDC アプリのポリシーが、メインアプリの利用者だけを許可していることを確認する。

Access の設定は、ホスト名・パス単位、Worker 単位、Workers 全体の順に具体的なものが優先される。Workers 全体を保護していても、より具体的な設定で公開するとその経路が保護されないため、設定変更時は手順 4・5 も確認する。

許可ユーザーの追加・削除

  1. Zero Trust の Access controls > Policies を開く。
  2. メインアプリの SaaS OIDC アプリに適用しているポリシーを開く。Workers 全体の Access と同じ再利用可能なポリシーを使っている場合は、プレビュー URL の許可対象も同時に変わる。
  3. Include ルールのメールアドレスまたはグループを更新して保存する。
  4. 利用者を削除したときは、管理画面(canshi-admin.cateiru.dev)でそのユーザーを「すべての端末からログアウト」させる。ポリシーから外しても、ログイン済みのセッションは有効期限まで使えるためである。
  5. 次の動作確認を実施する。

許可対象の具体的なメールアドレスやグループは機密性のある運用情報として扱い、リポジトリには記載しない。

動作確認

  • ログアウト状態で本番のカスタムドメインを開き、Access の画面ではなく CANSHI のログイン画面(/login)が表示されることを確認する
  • 許可対象外のアカウントで /login からログインし、Access の拒否画面が表示されてログインできないことを確認する
  • ログアウト状態または許可対象外のアカウントでプレビュー URL を開き、Access のログイン画面または拒否画面が表示されることを確認する
  • 許可対象のアカウントでログインし、CANSHI を利用できることを確認する
  • Worker や公開経路を追加したとき、および Access ポリシーを変更したときは再確認する

詳細は Cloudflare Access for Workers と Access policies を参照する。

Web Push(VAPID 鍵)

29 の Web Push 送信(src/features/push/)に使う VAPID 鍵ペアは、デプロイ環境ごとに1回生成する。

pnpm vapid:generate

出力される2つの値の扱いが異なる点に注意する。

  • NEXT_PUBLIC_VAPID_PUBLIC_KEY:クライアント(PushSubscriptionToggle)に埋め込まれるため、ビルド時に読める場所(pnpm cf:build を実行する環境の .env.production.local や CI の環境変数)に設定する必要がある。あわせて実行時(src/workflows/notification.ts が Push 送信に使う)にも必要なので、wrangler secret put でも設定する
  • VAPID_PRIVATE_KEY:サーバー側だけで使う秘密鍵。wrangler secret put で設定する(クライアントに公開してはいけない)
  • VAPID_SUBJECT:RFC 8292 が要求する連絡先(mailto: の管理者アドレスなど)。wrangler secret put で設定する
wrangler secret put NEXT_PUBLIC_VAPID_PUBLIC_KEY
wrangler secret put VAPID_PRIVATE_KEY
wrangler secret put VAPID_SUBJECT

いずれかが未設定の環境では、通知の生成(28)は動作するが Push 送信は自動的にスキップされる(getVapidKeys が null を返す)。

ローカル開発では .dev.vars(Git 管理対象外)に同じ3つを設定する。NEXT_PUBLIC_VAPID_PUBLIC_KEY は next dev/next build のビルド時読み込み用に .env にも追記する(.env.example 参照)。

エントリポイントと定期実行

wrangler.toml の main は .open-next/worker.js を直接ではなく、それをラップするカスタムエントリポイント worker.ts(リポジトリルート)を指す。worker.ts は OpenNext の fetch ハンドラに加えて scheduled([triggers] crons、15分ごと)と NotificationWorkflow([[workflows]])を export する。デプロイ後、cron トリガーが実際に有効になるまで最大15分のグローバル伝播遅延がある。

ローカルでの動作確認手順は README.md を参照。

MCP サーバー(外部 AI エージェント連携)

docs/plans/32_mcp_oidc_overview.md の方針に基づき、ChatGPT などの外部 AI エージェントが CANSHI のデータを参照できる MCP サーバーを、メインアプリの Worker の /mcp で公開している (src/features/mcp/)。コネクタに登録する URL は https://canshi.cateiru.dev/mcp。 認可サーバーの実装は @cloudflare/workers-oauth-provider で、ログインはメインアプリの ログイン(セッション)をそのまま使う。

  • src/worker.ts が OpenNext のハンドラを OAuthProvider で包む。/mcp(アクセストークンで 保護)・/oauth/token・/oauth/register(DCR)・/.well-known/oauth-* は Next.js に渡さずに 処理する
  • /oauth/authorize は同意画面。Worker が認可リクエストを検証してから Next.js のページ (src/app/oauth/authorize/page.tsx)を表示する。未ログインならログイン画面へ送り、ログイン後に 同意画面へ戻る。「許可する」を押すと、ログイン中のユーザーの ID をトークンに保存して認可を完了する (src/features/mcp/presentation/authorize.ts)。DCR は誰でも呼べるため、一度許可したクライアントでも 毎回同意画面を表示する
  • クライアントの登録は、MCP の 2026-07-28 版で推奨される Client ID Metadata Document(CIMD、 クライアントの情報を置いた HTTPS の URL を client_id にする方式)と、従来の Dynamic Client Registration(DCR、/oauth/register)の両方を受け付ける。CIMD の文書を取得するため、 wrangler.toml の互換性フラグに global_fetch_strictly_public を入れている。CIMD のクライアントは 同じアプリのすべての接続で client_id が同じため、接続ごとの読み取りの範囲もアプリごとに共有する
  • アクセストークンは MCP サーバーの URL(https://canshi.cateiru.dev/mcp、RFC 8707 の resource)に 紐づける(src/worker.ts の resourceMetadata、オリジンごとに決める)。 @cloudflare/workers-oauth-provider を 0.10 系から 1 系へ上げたとき、これと違う resource に 紐づいていた接続は、トークンの更新で invalid_grant になり、一度だけ接続し直しになる
  • MCP のツールは、トークンに保存したユーザーの家の猫だけを返す
  • クライアント登録・認可・トークンは D1 の oauth_kv_entries テーブルに保存する。 @cloudflare/workers-oauth-provider は Workers KV(env.OAUTH_KV)に保存する作りのため、 KV と同じ操作を D1 で実装したもの(src/features/mcp/infrastructure/d1KvStore.ts)を OAUTH_KV として渡している。 期限切れの行は定期実行(15 分ごとの scheduled)で削除する
  • MCP サーバーは MCP の 2026-07-28 版の仕様に沿ってセッションを持たず、リクエストごとに 作り直して応答する(src/features/mcp/presentation/agent.ts、MCP の SDK v2 と agents の createMcpHandler())。2025-11-25 版など initialize で始めるそれより前の版のクライアントにも、 同じくセッションを持たずに応答する
  • 以前はセッション状態を Durable Object(CanshiMcp、バインディング名 MCP_OBJECT)に 保存していた。使わなくなったため、wrangler.toml の [[migrations]](tag v2)で削除する。
  • メインアプリの本番ドメインは Access で保護していないため、/mcp などには Access のログイン画面を 挟まずに到達できる。プレビュー URL は Access で保護しているため、プレビューでは MCP を使えない

next dev は src/worker.ts を通らないため、/mcp と同意画面は使えない。ローカルで確認するときは pnpm cf:preview で起動する。

以前は別の Worker(packages/mcp-server、Worker 名 canshi-mcp、mcp.canshi.cateiru.dev)で 動かし、Cloudflare Access の SaaS OIDC アプリでログインしていた。メインアプリへ移したあとは 使わないため、Worker(とカスタムドメイン)を削除し、そのとき作った MCP 用の SaaS OIDC アプリと mcp.canshi.cateiru.dev/callback の Redirect URL、旧 Worker の KV Namespace(OAUTH_KV)も 不要になる。コネクタは新しい URL で接続し直す。

トラブルシューティング

ChatGPT からの接続だけ Dynamic client registration failed: registration endpoint returned 403 になる

POST /register(DCR)を curl 等で直接叩くと成功するのに、ChatGPT のカスタム コネクタから接続しようとすると DCR が 403 で失敗する場合、Cloudflare の Bot Fight Mode が ChatGPT からのリクエストをブロックしている可能性が高い。

  • @cloudflare/workers-oauth-provider のコードには DCR 失敗時に 403 を返す 経路が存在しない(バリデーションエラーは 400 系)ため、アプリケーション側の 問題ではなく Cloudflare のエッジで止められている、という切り分けができる
  • 同種の事例(solectrus/solectrus の MCP.md)でも、Claude は通るが ChatGPT だけ弾かれる、 という同じ症状が報告されている
  • 無料プランの Bot Fight Mode はパス単位で除外できないため、対処は以下のいずれか
    • Free プラン: ゾーン全体で Bot Fight Mode をオフにする (Cloudflare ダッシュボード → 対象ゾーン → Security → Bots)。 CANSHI の本番(cateiru.dev ゾーン)はこの方法で解決した
    • Pro プラン以上: Super Bot Fight Mode に切り替え、 /mcp・/oauth/*・/.well-known/oauth* にスキップルールを追加する

管理画面

運用者向けの管理画面を、メインアプリとは別の Cloudflare Workers(packages/admin/、Worker 名 canshi-admin)として用意している。公開ホスト名は canshi-admin.cateiru.dev (packages/admin/wrangler.jsonc の routes で custom_domain として設定。本番の workers.dev は 作らない。Worker Previews の URL だけは workers.dev に作る。「管理画面の自動デプロイとプレビュー」節)。メインアプリと同じ D1(canshi-db)と R2(canshi-media)を直接参照し、 ユーザー・家の一覧を閲覧できる。書き換えるデータは次のものだけ。

  • ユーザーをすべての端末からログアウトさせる操作で削除するログインセッション(sessions テーブル)
  • 家のメンバーの追加・削除(household_members テーブル)
  • 共通のごはん商品の登録・編集・削除(catalog_food_products テーブルと、その画像の media_assets の行・R2 のオブジェクト)

アクセス制御は Cloudflare Access に任せ、アプリ内では認証しない。管理画面は家ごとの絞り込みを せず全ユーザー・全家の情報を表示するため、メインアプリの利用者(家族など)を含む Workers 全体の Access のポリシーではなく、管理者だけを許可する専用のポリシーで保護する。

canshi-admin.cateiru.dev の Self-hosted アプリケーションを作れば、Workers 全体の Access より 具体的な設定として優先される(「Cloudflare Access 設定」節)。また、以前の MCP サーバーの カスタムドメイン(mcp.canshi.cateiru.dev)には Workers 全体の Access が適用されなかったため、 Workers 全体の Access だけには頼れない。Self-hosted アプリケーションは デプロイする前に作る(デプロイしてから作ると、それまでの間に一覧が誰にでも見えてしまう)。

初回の公開手順

  1. Zero Trust の Access controls > Policies で、管理者(運用者)だけを Include するポリシーを作る。 Workers 全体の Access に適用している、メインアプリの利用者全員を許可するポリシーは使わない。
  2. Access controls > Applications で Self-hosted アプリケーションを追加し、ドメインに canshi-admin.cateiru.dev を指定して、1. のポリシーだけを付ける。
  3. pnpm --filter @canshi/admin run deploy でデプロイする(カスタムドメインも作成される)。
  4. すぐに次の動作確認を実施する。

動作確認

  • ログアウト状態で https://canshi-admin.cateiru.dev/ を開き、Access のログイン画面が表示される ことを確認する(一覧が表示されたら、直ちに Worker を削除する)
  • 管理者ではないメインアプリの利用者のアカウントで認証し、拒否されることを確認する
  • 管理者のアカウントで認証し、ユーザー・家の一覧を閲覧できることを確認する
  • Access のポリシーやアプリケーションを変更したときは再確認する

管理画面のコードはマイグレーションを持たない。メインアプリのマイグレーションで列名を変えたときは、 管理画面も合わせて更新・デプロイする(packages/admin/src/db/queries.ts・ packages/admin/src/db/catalogFoodProducts.ts)。

管理画面の自動デプロイとプレビュー

管理画面も、メインアプリと同じく Cloudflare 側の GitHub App(Workers Builds)でデプロイする。 GitHub Actions からはデプロイしない。canshi-admin の Worker を同じリポジトリにつなぎ、 ビルドの対象のパス(Build watch paths)で packages/admin などが変わったときだけビルドする。

  • main にマージされ、管理画面に関わるファイルが変わっていれば、wrangler deploy で本番にデプロイされる
  • main 以外のブランチで管理画面に関わるファイルが変わっていれば、Worker Previews(wrangler preview)で プレビューが作られる。URL は <ブランチ名>-canshi-admin.<サブドメイン>.workers.dev
  • プレビューは本番のバインディングを引き継がない。packages/admin/wrangler.jsonc の previews で、 メインアプリのプレビューと同じプレビュー専用の D1(canshi-preview-db)・R2(canshi-media-preview)を 使う。本番のデータには触れない。プレビュー用 D1 のマイグレーションはメインアプリの pnpm db:migrate:preview で適用する(「プレビュー用 D1 のマイグレーション」節)
    • プレビュー用 D1 はメインアプリのプレビューと共有するため、管理画面のプレビューで行った書き換え (ユーザーの権限・家のメンバー・共通のごはん商品など)は、メインアプリのプレビューにも反映される
  • 管理画面はアプリ内で認証しないため、プレビューの URL・バージョンの URL も管理者だけに見せる。 canshi-admin の Worker 単位の Access(Previews only。Zero Trust では canshi-admin - Cloudflare Workers、destination が preview_worker)に管理者だけを許可するポリシーを 付けて保護する。本番のカスタムドメインは canshi-admin.cateiru.dev の Self-hosted アプリケーションが守る

wrangler.jsonc の preview_urls は wrangler deploy(本番のデプロイ)のときだけ反映される。

初回の設定手順

1. と 2. は、preview_urls: true を含む変更を main にマージする前に行う。 先にマージすると、Workers 全体の Access(メインアプリの利用者全員を許可する)だけで守られた バージョンの URL から、本番の D1 を読み書きする管理画面に入れてしまう。

  1. Workers & Pages → canshi-admin → Access で Protect this Worker behind Access を選び、 Previews only にして、管理者だけを許可するポリシーを付ける(2026-11-08 に設定済み)。ダッシュボードで選べるポリシーが 合わないときは、作られた Access アプリケーションを Zero Trust で開き、「初回の公開手順」の 1. で作った 管理者だけを Include するポリシーに付け替える。メインアプリの利用者全員を許可するポリシーは付けない。
  2. Workers & Pages → canshi-admin → Settings → Build で、GitHub の cateiru/canshi をつなぐ (2026-11-08 に設定済み)。つないだあとの値は、Cloudflare CLI(cf)の cf builds workers get <Worker のタグ> で確かめ、cf builds workers update <Worker のタグ> --body '<JSON>' で production_settings・previews_base_config をまとめて変えられる(Worker のタグは cf workers scripts search --name canshi-admin の id)。
    • プレビューのビルドの設定は、ブランチの最初のビルドのときに previews_base_config からブランチごとの トリガーにコピーされる。あとから previews_base_config を変えても既存のブランチには効かないため、 --patch-existing-previews true を付けて既存のプレビューにも当てる(付けずに変えると、既存のブランチは 古い設定のままビルドされる)。やり直すときは cf builds create <トリガーの UUID> でビルドする
    • Production branch:main
    • Root directory:packages/admin
    • Build variables:SKIP_DEPENDENCY_INSTALL を true(依存は次のビルドコマンドで入れる)
    • Build command:pnpm install --frozen-lockfile(ワークスペース全体の依存が入る)
    • Deploy command:pnpm run deploy(wrangler deploy)
    • Non-production branch deploy command:pnpm run preview(wrangler preview --ignore-base-config) (Worker Previews への切り替えを求められたら切り替える。切り替えは元に戻せない)。 どちらも packages/admin/package.json の scripts で、フラグはリポジトリで管理する。 run を省くと pnpm deploy が pnpm 組み込みのコマンドとして扱われて失敗するため、必ず付ける
    • Build watch paths の Include:packages/admin/*・pnpm-lock.yaml・pnpm-workspace.yaml。 ルートの package.json は PR ごとにバージョンを上げるため含めない
    • ダッシュボードの Previews Base には本番の設定をインポートしない(「プレビュー(Worker Previews)」節と同じ理由)
  3. この変更を main にマージし、canshi-admin のビルドが成功して本番にデプロイされたことを確かめる。 最初のビルドのログで、依存が pnpm で入っていることも確かめる。
  4. ログインしていないブラウザ(シークレットウィンドウなど)で次を開き、Access のログイン画面が出ることを 確かめる。管理者ではないメインアプリの利用者のアカウントで認証し、拒否されることも確かめる。 一覧が表示されたら、直ちに wrangler.jsonc の preview_urls を false に戻してデプロイする。
    • https://canshi-admin.cateiru.dev/
    • 管理画面を変えたブランチのプレビューの URL と、そのデプロイの URL(ビルドのログに出る)
  5. 最初のプレビューのビルドのログで、DB が canshi-preview-db、MEDIA_BUCKET が canshi-media-preview を指していることを確かめる。

メインアプリ(canshi)の Workers Builds の Build watch paths の Exclude に packages/admin/* を 入れると、管理画面だけを変えた PR でメインアプリのビルドが走らなくなる(任意)。

手動でデプロイするとき

Workers Builds を使わずにデプロイするときは、main のきれいな作業ツリーで pnpm --filter @canshi/admin run deploy を実行する。run を省くと pnpm 組み込みの pnpm deploy として 扱われ、ERR_PNPM_INVALID_DEPLOY_TARGET で失敗する。

監視・運用(TODO)

  • エラー監視・ログの方針
  • D1・R2 の利用量・コストの監視
  • バックアップ方針(D1・R2)

備考

本ファイルは実装の進行に合わせて更新する。各 TODO は該当する実装 PR(docs/plans/)に着手する前に内容を確定させること。