VercelでもWebSocketが使える!partyserver + Cloudflare Workersで無料リアルタイム通信
TL;DR
- Vercel の Serverless Functions は WebSocket 非対応だが、partyserver + Cloudflare Workersで無料のリアルタイム通信を実現できる。
- 5 分で「自分の Cloudflare アカウントにデプロイ」+「Next.js クライアント」まで動く最短手順を紹介。
- Cloudflare 公式が 2024 年 4 月に PartyKit を買収。
partyserverは Cloudflare Workers 向けのライブラリとして提供されている。
はじめに
Vercel を使っていて、リアルタイム機能を実装しようとした経験はありませんか?
残念ながら、Vercel Serverless Functions は WebSocket 接続を直接サポートしていません。
しかし、諦める必要はありません。partyserver + Cloudflare Workersの組み合わせで、無料でリアルタイム通信機能を実現できます。
PartyKit と partyserver の違い(2024 年 4 月買収以降)
この買収後、PartyKit エコシステムは 2 つのデプロイ方法を提供しています:
| 方式 | デプロイ先 | ツール | 料金 |
|---|---|---|---|
| PartyKit Cloud | PartyKit のマネージドプラットフォーム | npx partykit deploy |
無料枠あり(制限付き) |
| Cloudflare Workers | 自分の Cloudflare アカウント | npx wrangler deploy |
Cloudflare の無料枠 |
PartyKit の創設者 Sunil Pai 氏は元 Cloudflare 社員で、PartyKit はもともと Cloudflare の Durable Objects を活用しやすくするために作られたプロジェクトでした。今回の買収で「里帰り」した形です。
partyserver とは
partyserverは、Cloudflare Workers 上でリアルタイム・マルチユーザーアプリケーションを簡単に構築できるライブラリです。
主なユースケース
- 協調編集 - Google Docs のような同時編集
- マルチプレイヤーゲーム - リアルタイム対戦
- チャット - リアルタイムメッセージング
- AI エージェント - ストリーミング応答
- ローカルファースト - オフライン対応アプリ
なぜ Cloudflare Workers + partyserver なのか
技術的な背景
partyserver は内部的にCloudflare Durable Objectsを使用しています。
Durable Objects とは:
- ステートフルな Serverless Functions
- クライアント間で状態を同期可能
- グローバルエッジで実行
partyserver はこの Durable Objects を抽象化し、より簡単にリアルタイム機能を実装できるようにしたものです。
これから作るゴール
-
my-party.<account>.workers.devにデプロイされた partyserver が稼働 - Next.js (Vercel) から partysocket 経由で接続しチャットが動く
- Cloudflare の無料枠の範囲で運用開始
┌─────────────────┐ ┌─────────────────┐
│ Vercel │ │ Cloudflare │
│ (Frontend) │◀────────▶ │ (PartyKit) │
│ Next.js │ WebSocket │ Durable Objects│
└─────────────────┘ └─────────────────┘
最短クイックスタート(6 ステップ)
-
Cloudflare アカウント作成
https://dash.cloudflare.com/sign-up -
プロジェクト作成 & パッケージインストール
mkdir my-party && cd my-party npm init -y npm install hono hono-party partyserver npm install -D wrangler typescript @cloudflare/workers-types -
wrangler.json を作成
wrangler.json{ "$schema": "node_modules/wrangler/config-schema.json", "name": "my-party", "main": "src/index.ts", "compatibility_date": "2025-01-01", "durable_objects": { "bindings": [ { "name": "ChatRoom", "class_name": "ChatRoom" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": ["ChatRoom"] } ] } -
サーバー実装を配置(後述の
src/index.tsをコピペ) -
デプロイ
npx wrangler deploy初回は Cloudflare ログインが求められます。
-
接続テスト
npx wscat -c wss://my-party.<account>.workers.dev/parties/chatroom/general{"type":"ping"}を送って{"type":"pong"}が返れば OK。
サーバー実装(hono-party 使用)
実用的な最小構成。タイピングインジケーターや Server Actions 用 HTTP エンドポイントも含む。
import { Hono } from "hono";
import { cors } from "hono/cors";
import { partyserverMiddleware } from "hono-party";
import { Server } from "partyserver";
import type { Connection, WSMessage } from "partyserver";
/**
* チャットルーム: ルームごとのリアルタイムメッセージ配信
* 接続URL: /parties/chatroom/{roomId}
*/
export class ChatRoom extends Server {
// 接続中のユーザー数
private getConnectionCount(): number {
return [...this.getConnections()].length;
}
onConnect(conn: Connection) {
// 接続時に確認メッセージを送信
conn.send(
JSON.stringify({
type: "connected",
roomId: this.name,
connectionCount: this.getConnectionCount(),
})
);
}
onMessage(conn: Connection, message: WSMessage) {
if (typeof message !== "string") return;
try {
const data = JSON.parse(message);
// ping/pong でコネクション維持
if (data.type === "ping") {
conn.send(JSON.stringify({ type: "pong" }));
return;
}
// タイピングインジケーターは送信者以外にブロードキャスト
if (data.type === "typing") {
this.broadcast(message, [conn.id]);
return;
}
// 他のメッセージは全クライアントにブロードキャスト
this.broadcast(message);
} catch {
console.error("Failed to parse message:", message);
}
}
// HTTP経由でメッセージを受信(Server Actionsから呼び出し用)
async onRequest(req: Request): Promise<Response> {
if (req.method === "POST") {
try {
const data = await req.json();
this.broadcast(JSON.stringify(data));
return new Response(
JSON.stringify({
success: true,
connectionCount: this.getConnectionCount(),
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
} catch {
return new Response(JSON.stringify({ error: "Invalid request" }), {
status: 400,
headers: { "Content-Type": "application/json" },
});
}
}
// GET: 接続状態を返す
if (req.method === "GET") {
return new Response(
JSON.stringify({
roomId: this.name,
connectionCount: this.getConnectionCount(),
}),
{
status: 200,
headers: { "Content-Type": "application/json" },
}
);
}
return new Response("Method not allowed", { status: 405 });
}
}
// Honoアプリケーション
const app = new Hono<{ Bindings: { ChatRoom: DurableObjectNamespace } }>();
// CORS設定
app.use(
"*",
cors({
origin: ["http://localhost:3000", "https://your-app.vercel.app"],
allowMethods: ["GET", "POST", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization"],
})
);
// ヘルスチェック
app.get("/health", (c) => c.json({ status: "ok" }));
// partyserverミドルウェア(/parties/* へのリクエストをルーティング)
app.use("*", partyserverMiddleware());
export default app;
この実装のポイント
-
onConnect: 接続時に確認メッセージと接続数を返す -
onMessage: ping/pong、タイピングインジケーター、通常メッセージを処理 -
onRequest: HTTP 経由でのブロードキャスト(Server Actions からの呼び出しに便利) - CORS 設定: Vercel 側のオリジンを許可
-
URL パス: hono-party はデフォルトで
/parties/{server}/{room}形式の URL をルーティング
クライアント側の実装(Vercel / Next.js)
partysocket のインストール
npm install partysocket
React コンポーネント
"use client";
import { usePartySocket } from "partysocket/react";
import { useState } from "react";
type Message = {
type: "connected" | "message" | "typing";
userId?: string;
content?: string;
roomId?: string;
connectionCount?: number;
};
export default function ChatPage() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const socket = usePartySocket({
// Cloudflare Workersにデプロイした際のホスト名
host: "my-party.<your-account>.workers.dev",
// partyserverのクラス名(小文字)
party: "chatroom",
// ルーム名
room: "general",
onMessage: (event) => {
const data: Message = JSON.parse(event.data);
setMessages((prev) => [...prev, data]);
},
});
const sendMessage = () => {
if (input.trim()) {
socket.send(
JSON.stringify({
type: "message",
content: input,
userId: "user-" + Math.random().toString(36).slice(2, 6),
})
);
setInput("");
}
};
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">チャットルーム</h1>
<div className="border rounded p-4 h-64 overflow-y-auto mb-4">
{messages.map((msg, i) => (
<div key={i} className="mb-2">
{msg.type === "message" ? (
<p>
<strong>{msg.userId}:</strong> {msg.content}
</p>
) : msg.type === "connected" ? (
<p className="text-green-500 text-sm">
接続しました(ルーム: {msg.roomId})
</p>
) : null}
</div>
))}
</div>
<div className="flex gap-2">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
className="flex-1 border rounded px-3 py-2"
placeholder="メッセージを入力..."
/>
<button
onClick={sendMessage}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
送信
</button>
</div>
</div>
);
}
デプロイ後の動作確認チェックリスト
-
GET https://my-party.<account>.workers.dev/healthが{ "status": "ok" }を返す -
npx wscat -c wss://my-party.<account>.workers.dev/parties/chatroom/generalで接続できる -
{"type":"ping"}を送信して{"type":"pong"}が返る - Next.js ページで 2 タブ開き、片方の入力がもう片方に即時反映される
-
CORS エラーが出ない(
origin設定を確認)
料金と代替サービス(クイック表)
| サービス | 料金感(個人〜小規模) | 強み | 向き |
|---|---|---|---|
| partyserver + Cloudflare | DO は無料枠、従量も低額 | エッジ × ステートフル、OSS | 汎用リアルタイム |
| Ably | 無料枠少、小さく有料 | 高信頼メッセージング | 大規模・SLA |
| Pusher | 無料枠あり | 導入容易 | 通知・小規模 |
| Liveblocks | 無料枠あり | 協調編集 UI 特化 | 共同編集 |
| Supabase Realtime | 無料枠あり | OSS/PostgreSQL 連携 | DB 駆動更新 |
| Convex | 無料枠あり | バックエンド一体型 | フルスタック |
まとめ
- Vercel Serverless Functions は WebSocket を直接サポートしていない
- partyserver + Cloudflare Workersで無料リアルタイム通信を実現できる
- 2024 年 4 月の買収により、PartyKit エコシステムはCloudflare に完全統合
-
partyserverを使って自分の Cloudflare アカウントにデプロイし、使った分だけ支払う(無料枠あり) - hono-partyを使えば Hono アプリケーションに簡単に統合できる
- まずはクイックスタートの 6 ステップでデプロイし、
wscatと Next.js で動作確認しよう
リアルタイム機能が必要になったら、ぜひ partyserver + Cloudflare Workers を試してみてください。
Discussion