Vercel + Supabase Realtime (Broadcast) で 2 人対戦ピンポンゲームを動かす

Vercel + Supabase Realtime (Broadcast) で 2 人対戦ピンポンゲームを動かす

Vercel と Supabase Realtime (Broadcast) を組み合わせ、Vercel にデプロイできる 2 人対戦ピンポンゲームの最小構成を紹介します。検証ではゲスト側の入力欠落をほとんど感じませんでした。また Supabase Integration による環境変数の自動同期が便利でした。
2025.12.20

はじめに

本記事では、Vercel にデプロイできる最小構成の 2 人対戦ピンポンゲームを題材に、既存の Pub/Sub 部分を Supabase Realtime (Broadcast) へ置き換えて動作させた結果を報告します。前回の記事 では Momento Topics で同じデモを構成しました。本記事の目的は、これを Supabase Realtime に置き換えたうえで、同様に動作検証することです。

vercel-realtime-game-demo

Vercel とは

Vercel は、Next.js を中心とした Web アプリケーションを Git 連携で継続的にデプロイできるホスティング基盤です。フロントエンドと API (Route Handlers) を同一プロジェクトで扱えるため、デモ用途でも検証を進めやすい点が特徴です。

Supabase Realtime とは

Supabase Realtime は、クライアント間でイベントを配信できるリアルタイム基盤です。Broadcast は、任意のチャンネルに対してメッセージを送受信でき、クライアントライブラリでは WebSocket 接続を用いて配信します。

Vercel の Supabase Integration では、Vercel Marketplace から Supabase を連携し、Supabase プロジェクトの環境変数を Vercel プロジェクトへ自動同期できます。 また、Preview Deployment を使う構成では、Preview 向けの redirect URL を自動で用意する仕組みもあり、検証時の設定作業を減らせます。

対象読者

  • Vercel にデプロイできるリアルタイム通信の最小構成を知りたい方
  • Supabase Realtime (Broadcast) の基本的な送受信方法を動くコードで確認したい方
  • Vercel の Supabase Integration で環境変数を同期し検証したい方

参考

構成

Vercel の Functions は常駐プロセスではないため、Vercel におけるリアルタイム通信では、クライアントが外部の Realtime 基盤へ subscribe し、Functions は publish 側に寄せる設計が推奨されています。Vercel の公式ドキュメント でも、Functions はできるだけ短時間で応答し、subscribe を担わない構成が推奨されています。

今回のデモもこの方針に合わせ、ブラウザ同士のリアルタイム通信を Supabase Realtime (Broadcast) に寄せます。Vercel 側はルーム作成や参加の入口だけを持ち、ゲーム中のメッセージ交換はチャンネル経由で行います。

実装

Supabase プロジェクトの準備

最初に Supabase 側でプロジェクトを作成します。

create suoabase project

ここで発行される URL と key を、クライアント初期化に使用します。

supabase url and key

Supabase クライアントの追加

前回記事 にて、Momento Topics を利用していた送受信処理を、Supabase Realtime (Broadcast) の送受信処理へ置き換えました。

Broadcast の受信においては、 channel.on ( ... ) でハンドラを登録し、channel.subscribe ( ... ) でチャンネルに参加します。送信には channel.send ( ... ) を使用します。本デモではブラウザのクライアントライブラリから送受信し、WebSocket を使って配信します。

Broadcast の受信と送信の骨格 (connectToSupabase 抜粋)
const channelName = `paddle-game:${roomIdToJoin}`;
channelNameRef.current = channelName;

// Create Supabase Realtime channel
const channel = getSupabase().channel(channelName);

// Subscribe to broadcast events
channel.on("broadcast", { event: "game" }, (payload) => {
  const message = payload.payload as TopicMessage;
  handleTopicMessage(message, playerRole);
});

// Subscribe to the channel
await channel.subscribe((status) => {
  if (status === "SUBSCRIBED") {
    console.log(`[Game] ${playerRole}: Successfully subscribed to ${channelName}`);
  }
});

channelRef.current = channel;

// Wait a short time for subscription to be established
await new Promise((resolve) => setTimeout(resolve, 500));

// If guest, send join message after subscribing
if (playerRole === "guest") {
  const joinMessage: JoinMessage = {
    type: "join",
    clientId,
    t: Date.now(),
  };
  await channel.send({
    type: "broadcast",
    event: "game",
    payload: joinMessage,
  });
}

Broadcast は Supabase のクライアントライブラリから送受信します。送信は subscribe 前は HTTP、subscribe 後は WebSocket を使用します。本記事では、Vercel 上の Next.js アプリから扱いやすい構成として、クライアントライブラリ経由の送受信に絞って説明します。

このデモでは、ローカルと Vercel の両方で同じ初期化コードを使えるよう、NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY を用いてクライアントを生成します。

クライアント生成 (components/RealtimePaddleGame.tsx 抜粋)
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";

let _supabaseClient: ReturnType<typeof createClient> | null = null;

function getSupabase() {
  if (!_supabaseClient) {
    if (!supabaseUrl || !supabaseAnonKey) {
      throw new Error(
        "Missing Supabase environment variables. Please set NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY."
      );
    }
    _supabaseClient = createClient(supabaseUrl, supabaseAnonKey);
  }
  return _supabaseClient;
}

RealtimePaddleGame の置き換え方針

置き換え作業は、できるだけ既存のメッセージ設計を保つ方針で進めます。今回の実装では、メッセージ種別 (join, start, input, state, game_end) と送信頻度、Host Authoritative の役割分担、描画や TICK (50 ms) のロジックは維持し、送受信 API だけを差し替えました。

型定義 抜粋
interface JoinMessage {
  type: "join";
  clientId: string;
  t: number;
}

interface StartMessage {
  type: "start";
  t: number;
}

interface InputTopicMessage {
  type: "input";
  up: boolean;
  down: boolean;
  t: number;
}

interface StateMessage {
  type: "state";
  state: GameState;
  t: number;
}

interface GameEndMessage {
  type: "game_end";
  state: GameState;
  t: number;
}

type TopicMessage =
  | JoinMessage
  | StartMessage
  | InputTopicMessage
  | StateMessage
  | GameEndMessage;

チャンネル名は、旧 topic 名と同様に paddle-game:${roomId} を採用し、同じ roomId を共有した 2 クライアントが同一チャンネルへ参加する形にします。

ルーム API の扱い

Supabase Realtime (Broadcast) 自体は、ルームの満員判定や host / guest の役割確定といったルーム管理機能を提供しません。そのため、もしルームを厳密に管理したいという場合であれば、別途データベースやストレージと組み合わせた制御が必要になります。

今回は最小限の構成で検証したかったというのもあり、ルーム管理についてはいったんスコープ外として進めています。検証の焦点を Pub/Sub の置き換えに絞るため、POST /api/roomsroomId の生成のみ、POST /api/rooms/join は常に成功を返す形へ簡略化しました。

roomId 生成のみ (app/api/rooms/route.ts 抜粋)
export async function POST(request: NextRequest) {
  const body = (await request.json()) as CreateRoomRequest;
  const { clientId } = body;

  if (!clientId) {
    return NextResponse.json({ error: "clientId is required" }, { status: 400 });
  }

  const roomId = nanoid(8);

  return NextResponse.json({ roomId });
}
常に成功を返す (app/api/rooms/join/route.ts 抜粋)
export async function POST(request: NextRequest) {
  const body = (await request.json()) as JoinRoomRequest;
  const { roomId, clientId } = body;

  if (!roomId || !clientId) {
    return NextResponse.json(
      { error: "roomId and clientId are required" },
      { status: 400 }
    );
  }

  // Always succeed for demo
  return NextResponse.json({ success: true });
}

Vercel の Supabase Integration と環境変数同期

Vercel にデプロイし、 Supabase の環境変数を同期します。

まず、GitHub リポジトリに push し、 Vercel 側で Project を作成して接続します。

create project

Vercel Marketplace で Supabase を検索し選択します。

Vercel Marketplace

Install をクリックします。

press install

今回はすでに Supabase プロジェクトがあるので、既存のプロジェクトを使用する設定で進めます。

link existing project

連携する Vercel プロジェクトを選択します。

manage access

Supabase 側のプロジェクトと、 Vercel 側のプロジェクトをそれぞれ選択します。

integration config

Integration が設定する環境変数には、NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY が含まれます。

検証

ローカルでの動作確認

ローカルでは、環境変数に NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY を設定し、開発サーバーを起動します。ブラウザを 2 タブ開き、片方で Create Room、もう片方で Room ID を入力して Join することで、2 人対戦の開始を確認します。

game top

CREATE ROOM をクリックし、部屋番号が表示されることを確認しました。

displayed number

guest 側で発行された番号を入力し、合流して対戦できることを確認しました。

join game

Vercel デプロイ後の確認

Vercel にデプロイした環境でも、同様に 2 タブで動作を確認しました

vercel-realtime-game-demo

考察

操作した所感

本稿の検証では、ゲームとして問題なくプレイできる操作感を確認できました。前回記事において、課題として記録したゲスト入力の欠落問題についても、今回の検証においては特に感じませんでした。

Integration が便利

Supabase Integration による自動同期は、Vercel へのデプロイ作業をシンプルにしてくれました。Vercel 側の Environment Variables に値を手入力せずに済むため、設定ミスを避けやすくなります。

Integration が設定する環境変数の一覧が明記されている (参考) ため、どの値が同期されるかを確認しながら作業できたのが良かったです。NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY のように、クライアント側で参照する値が最初から揃う点は、デモのような検証用途でも扱いやすいと感じました。

まとめ

Vercel と Supabase Realtime (Broadcast) を組み合わせ、Vercel にデプロイできる 2 人対戦ピンポンゲームを実装しました。検証ではゲスト側の入力欠落をほとんど感じず、快適にプレイできました。Supabase Integration により環境変数を Vercel に手入力せず同期でき、デプロイ作業をシンプルにできることが分かりました。

この記事をシェアする

FacebookHatena blogX

関連記事