アーキテクチャ
全体像
Browser (React)
↓
Cloudflare Workers (leader-collie)
└── React Router v7 framework モード (SSR)
├── workers/app.ts … fetch ハンドラ (createRequestHandler + setCloudflareEnv)
├── app/routes/protected.tsx … 認証ガード (旧 middleware 相当)
├── page routes (loader) … サーバーサイドのデータ取得
├── resource routes … /api/** (URL は旧 Next.js 時代と互換)
└── /api/actions/:domain … 旧 Server Actions のディスパッチャ
↓
Service Layer (lib/services/)
↓
Repository Layer (lib/repositories/)
↓
┌────────┴─────────┬───────────────┐
D1 `DB` D1 `ANALYSIS_DB` R2 `REPORTS_BUCKET`
(drizzle-orm/d1) (生 SQL) (メディア)Cloudflare binding へのアクセス方法
workers/app.tsの fetch ハンドラがリクエストごとにsetCloudflareEnv(env)(lib/cloudflare/context.server.ts) を呼び、サーバーコードはgetCloudflareEnv()で参照する- D1 (メイン):
lib/db/config.tsがdbを遅延 Proxy として公開。 最初のクエリ時にenv.DBから drizzle インスタンスを生成するため、 リポジトリ層は従来どおりimport { db } from "@/lib/db/config"で使える - D1 (分析):
lib/db/analysis-db.tsが旧 libsql Client と同じexecute({ sql, args }) → { rows }形のアダプタを提供。分析系リポジトリは生 SQL を実行する - R2:
lib/storage/r2-client.ts。サーバー内の delete / head は binding、 ブラウザ直アップロード用の presigned PUT URL は aws4fetch(S3 互換 API)で署名する - vars / secrets:
nodejs_compatにより Worker のprocess.envに注入されるため、 既存コードのprocess.env.XXX読み出しはそのまま動く
旧 Next.js 機能の置き換え
| 旧 | 新 |
|---|---|
| RSC ページ (async component) | route module の loader + 同期コンポーネント (useLoaderData) |
| Server Actions | lib/actions/<domain>.server.ts(第1引数に Request)。クライアントは app/actions/<domain>.ts の同名ラッパー → POST /api/actions/:domain |
| API Routes | app/routes/api/** の resource routes(loader = GET、action = POST/PUT/DELETE) |
| middleware の認証ガード | app/routes/protected.tsx(pathless layout)。/events/:id/report のみ公開ルートで、公開状態を loader がチェック(Slackbot OGP 対応) |
next/link | components/link.tsx(href prop 互換) |
next/navigation | hooks/use-router.ts(useRouter().push/refresh ほか互換) |
revalidatePath | React Router の revalidation(mutation 後の router.refresh() / ナビゲーション時の loader 再実行) |
ローカル開発の仕組み
react-router dev(@cloudflare/vite-plugin)が SSR を workerd 上で実行し、 wrangler.jsonc の binding と .dev.vars を提供する。 ローカル D1 の実体は .wrangler/state/v3/d1/.../<hash>.sqlite。
Workers ランタイムの外で動くツールは binding を使えないため、アクセス経路が分かれる:
| ツール | DB アクセス経路 |
|---|---|
seed / reset (lib/db/seed.ts / reset.ts) | lib/db/local-db.ts(node:sqlite + drizzle sqlite-proxy でローカル D1 の SQLite を直接操作) |
E2E フィクスチャ (e2e/fixtures/database.ts) | 同上(E2E_TEST_USER は e2e/global-setup.ts が .dev.vars 経由で Worker に注入) |
運用スクリプト (lib/scripts/: backup / sync-slack / fix-duplicate-users) | Cloudflare D1 HTTP API(lib/scripts/d1-http.mjs) |
データモデル
メイン DB(スキーマは共有パッケージ packages/leader-collie-db の src/schema.ts が所有し、drizzle-kit でマイグレーション生成):
users/sessions/accounts/verifications… better-auth + プロフィール (Slack ユーザー ID、Google カレンダートークン類を含む)events/tags/event_tags/event_candidates/votes/event_participants- レポート系(本文、添付メディア。メディアの実体は R2、
s3_keyカラムに R2 のオブジェクトキーを格納)
分析 DB(外部の分析パイプラインが書き込み、本アプリは読み取りのみ):
messages_vectors… Slack 投稿のメタデータ。分析ダッシュボードが集計クエリを実行する
主要な型:
EventStatus:"scheduling" | "confirmed" | "canceled"EventFormat:"online" | "offline" | "hybrid"VoteChoice:"◎" | "△" | "✕"UserRole:"admin" | "member"
外部サービス連携
すべて fetch ベース(Workers で動かない SDK は使わない):
- Slack: OAuth (better-auth) / users.list・users.info (
lib/slack/) / 通知 (TIED_BOT_URL経由) - Google Calendar: OAuth2 + Calendar v3 REST (
lib/services/google-calendar-service.ts) - OpenAI: AI SDK (
lib/services/report-ai-service.ts)。OPENAI_BASE_URLに AI Gateway の URL を設定するとゲートウェイ経由になる