Skip to content

アーキテクチャ

全体像

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.tsdb を遅延 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 Actionslib/actions/<domain>.server.ts(第1引数に Request)。クライアントは app/actions/<domain>.ts の同名ラッパー → POST /api/actions/:domain
API Routesapp/routes/api/** の resource routes(loader = GET、action = POST/PUT/DELETE)
middleware の認証ガードapp/routes/protected.tsx(pathless layout)。/events/:id/report のみ公開ルートで、公開状態を loader がチェック(Slackbot OGP 対応)
next/linkcomponents/link.tsxhref prop 互換)
next/navigationhooks/use-router.tsuseRouter().push/refresh ほか互換)
revalidatePathReact 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_USERe2e/global-setup.ts.dev.vars 経由で Worker に注入)
運用スクリプト (lib/scripts/: backup / sync-slack / fix-duplicate-users)Cloudflare D1 HTTP APIlib/scripts/d1-http.mjs

データモデル

メイン DB(スキーマは共有パッケージ packages/leader-collie-dbsrc/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 を設定するとゲートウェイ経由になる