OFTN.GG の技術構成

Taishi Naritomi

OFTN.GG は、参加者の募集から対戦表の作成、試合結果の報告までを扱うトーナメント運営サービスです。 画面を作るだけでなく、勝敗に応じて次の対戦相手を決めたり、主催者と参加者が見ている画面に更新を届けたりする必要があります。

今回は、その処理を支える技術構成を紹介します。2026年11月時点のコードと設定をもとに、画面、API、データベース、開発・デプロイ環境のつながりを説明します。

全体の構成

アプリケーションは TypeScript を中心に実装しています。主な構成は次のとおりです。

役割使用している技術
Web アプリケーションNext.js、React
UI・フォームBase UI、Tailwind CSS、CSS Modules、Conform、Zod
対戦表の描画React Flow
データ取得・キャッシュTanStack Query、graphql-request
リアルタイム更新・一部の更新処理Apollo Client、graphql-ws
APINode.js、Hono、GraphQL Yoga、Pothos
データベースPostgreSQL、Prisma
セッション・更新通知・ジョブRedis、BullMQ
ドキュメント・ブログNext.js、Fumadocs、MDX
リポジトリ・ビルド管理pnpm workspace、Turborepo

コードはモノレポで管理しています。サービス本体の画面を apps/web、API を apps/api、ドキュメントサイトを apps/docs に分けています。複数のアプリで使う処理は packages 配下のパッケージとして共有しています。

たとえば、トーナメントを生成する @oftn/tournament は、API と Web の両方から呼び出します。ブラウザでの対戦表のプレビューにも、サーバーでの対戦表の生成にも、同じ計算処理を使う構成です。

Next.js で画面とサーバー側の処理をつなぐ

Web アプリケーションは Next.js の App Router を使っています。サーバー側でログイン中のユーザーなどの情報を取得し、操作を受け付ける Client Component に渡しています。

画面の部品には Base UI を使い、Tailwind CSS と CSS Modules でスタイルを定義しています。フォームでは Conform と Zod を組み合わせ、入力値と検証結果を扱います。対戦一覧のページ番号や絞り込み条件を URL に持たせる部分には、nuqs を使っています。

対戦表のうち、エリミネーション形式の描画には React Flow を使っています。試合や参加者をノードに、勝ち上がりのつながりをエッジに変換して表示します。トーナメントの計算は @oftn/tournament、ノードの表示やズーム操作は React Flow という役割分担です。

データ取得と更新の購読

画面のデータ取得とキャッシュには TanStack Query を使っています。試合情報の取得では、次の順に処理が進みます。

画面の TanStack Query
    ↓
Next.js の Server Function
    ↓
graphql-request の生成 SDK
    ↓
GraphQL API
    ↓
Prisma → PostgreSQL

GraphQL Code Generator は、API のスキーマと画面側の GraphQL ドキュメントから、型情報と呼び出し用の SDK を生成します。API の入出力と画面で扱うデータの型を、スキーマに合わせて確認できるようにしています。

試合、チャット、通知の更新は、Apollo Client の GraphQL Subscription で購読します。ブラウザと API の間は graphql-ws による WebSocket 接続です。試合情報では、購読した更新を TanStack Query のキャッシュに反映します。

Apollo Client はイベントの作成・更新など、一部の Mutation にも使っています。データ取得とキャッシュ、更新の購読を、それぞれの実装に応じて組み合わせています。

Hono と GraphQL で API を提供する

API は Node.js 上で動く Hono アプリケーションです。GraphQL のリクエストは GraphQL Yoga が処理し、スキーマは Pothos を使って TypeScript で定義しています。

GraphQL には、データを取得する Query、変更する Mutation、更新を購読する Subscription を用意しています。入力値の検証には Zod を使います。認証関連の処理や Stripe の webhook には、Hono の HTTP ルートも使っています。

データの取得には DataLoader も組み込んでいます。イベント、参加者、試合などの取得をまとめて処理する仕組みで、GraphQL のフィールドごとに同じデータを個別取得することを減らしています。

試合結果がほかの画面に届くまで

主催者が試合結果を確定する操作を例にすると、API と画面は次のようにつながっています。

  1. GraphQL Mutation が、操作するユーザーの権限とイベントの状態を確認します。
  2. API が PostgreSQL のトランザクションを開始し、試合結果を保存します。
  3. 同じトランザクションの中で、勝ち上がりや関連する試合・フェーズの状態を更新します。
  4. トランザクションの完了後、Redis の Pub/Sub に更新通知を送ります。
  5. GraphQL Subscription が最新の試合情報を取得してブラウザに送り、画面側のキャッシュに反映します。

試合結果の保存と、それに伴う大会の状態変更をまとめて処理してから、閲覧中の画面に更新を届ける構成です。

大会のデータと計算処理

イベント、参加者、フェーズ、ブラケット、試合などの主要データは PostgreSQL に保存します。データモデルとマイグレーションは Prisma で管理しています。

大会固有の計算処理は @oftn/tournament にまとめています。シングルエリミネーション、ダブルエリミネーション、総当たり、FFA などの対戦表の生成に加え、スコア、ポイント、順位の計算も扱います。

参加者の配置方法については、別の記事「トーナメントにおける最適なプレイヤーの配置と実装方法」で紹介しています。こうした計算処理を画面やデータベースへのアクセスから分け、API とブラウザで共有しています。

プランごとの参加人数上限や利用できる機能も、@oftn/event と @oftn/policy で定義しています。API 側の制限の確認と、画面側の表示の判断に同じ定義を使います。

Redis と BullMQ の役割

Redis は、ログインセッションの保存、Subscription に向けた Pub/Sub、メール認証などのレート制限に使っています。BullMQ のジョブキューも Redis を使います。

ジョブの例には、参加者が申請した試合結果の承認期限を扱う処理があります。期限に合わせた遅延ジョブを登録し、時間が来たら worker が処理します。worker は API と同じ Node.js プロセス内で起動しています。

アップロード画像の保存・取得・削除には Amazon S3 を使っています。通知はデータベースに保存したうえで、メール、Web Push、Pub/Sub に展開します。メールテンプレートと通知文面も共有パッケージで管理しています。

このブログもモノレポの一部

ドキュメントサイトは、サービス本体とは別の Next.js アプリケーションです。Fumadocs と MDX を使い、操作マニュアル、ブログ、更新履歴をリポジトリ内で管理しています。

この記事も apps/docs/content/blogs に置いた MDX ファイルです。記事のタイトル、著者、日付、トピックをファイルの先頭に記述すると、既存の仕組みで記事ページ、ブログ一覧、RSS、サイトマップに反映されます。

お知らせや利用規約などの取得には microCMS を使っています。ドキュメントサイトの中で、リポジトリ内のコンテンツと CMS のコンテンツを扱っています。

開発環境とデプロイ

ローカル開発では、Docker Compose で PostgreSQL、Redis、S3Mock、Mailpit を起動します。画像保存やメール送信の動作も、開発用のサービスに接続して確認できる構成です。

アプリケーションの起動とビルドは Turborepo でまとめています。Git worktree ごとに利用するポートと Docker Compose のプロジェクト名を生成する仕組みがあり、複数の作業環境を並べて起動できます。

GitHub Actions の CI では、ビルド、型チェックや静的解析、Vitest によるテストとカバレッジ計測を実行します。静的解析とフォーマットには Biome を使っています。

リポジトリにある EC2 向けのデプロイ設定では、CodeBuild で API・Web・Prisma の配布物を作り、CodeDeploy で配置、マイグレーション、サービスの起動を行います。API と Next.js はそれぞれ Node.js のプロセスとして動かし、PM2 で管理します。Next.js の配布には standalone 出力を使っています。

エラーの収集には API と Web に Sentry を組み込んでいます。また、EC2 向けの設定には CloudWatch agent の配置と起動処理があります。

OFTN.GG では、画面、API、ドキュメントを分けつつ、大会の計算処理や機能制限の定義を共有しています。対戦表のプレビューとサーバーでの生成、結果の保存と画面への反映を、この構成でつないでいます。

  • 技術構成
  • TypeScript
  • Next.js
  • GraphQL
  • AWS
ブログ一覧