chat-widget について
リアルタイムのカスタマーサポートチャットを構築する開発者向けスキルです。ユーザー用ウィジェットと管理画面に加え、データモデル、API、WebSocket、通知、複数フレームワークの実装例を扱います。
主な機能
- 01フローティング式チャットウィジェット
- 02リアルタイムメッセージと接続状態
- 03管理者向けチャット一覧と詳細画面
- 04未読管理とアーカイブ操作
- 05遅延メール通知
活用シーン
- 01アプリ内サポートチャットの追加
- 02ユーザーと管理者間のリアルタイム通信
- 03サポート担当者による現行・アーカイブ済み会話の管理
- 04未読の管理者返信をメールで通知
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ shpigford/ skills --skill chat-widgetインストール後、Claude Code で/chat-widgetを入力して読み込まれたか確認します。
詳細
ライブサポートチャットウィジェット
ユーザー向けのフローティングウィジェットと、サポートスタッフ向けの管理ダッシュボードを備えたリアルタイムサポートチャットシステムを構築します。
このスキルを使用する場面
ユーザーが次のことを望む場合に使用します:
- アプリにライブチャットウィジェットを追加する
- カスタマーサポートチャット機能を構築する
- ユーザーと管理者の間のリアルタイムメッセージングを作成する
- アプリ内サポートチャネルを追加する
アーキテクチャの概要
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND │
├─────────────────────────────┬───────────────────────────────────┤
│ User Widget │ Admin Dashboard │
│ - Floating chat button │ - Chat list (active/archived) │
│ - Message panel │ - Conversation view │
│ - Unread badge │ - Archive/restore controls │
│ - Connection indicator │ - User info display │
└─────────────┬───────────────┴───────────────┬───────────────────┘
│ │
│ WebSocket + REST API │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ BACKEND │
├─────────────────────────────────────────────────────────────────┤
│ Channels │ Controllers │
│ - ChatChannel (per chat) │ - User: get/create chat │
│ - AdminChannel (global) │ - Admin: list, view, archive │
├─────────────────────────────┼───────────────────────────────────┤
│ Models │ Jobs │
│ - Chat (1 per user) │ - Email notification (delayed) │
│ - Message (many per chat) │ │
└─────────────────────────────────────────────────────────────────┘
実装ガイド
ステップ 1: データモデル
support_chats と support_messages の 2 つのテーブルを作成します。
support_chats
id - primary key (UUID recommended)
user_id - foreign key to users (UNIQUE - one chat per user)
last_message_at - timestamp (for sorting chats by recency)
admin_viewed_at - timestamp (tracks when admin last viewed)
archived_at - timestamp (null = active, set = archived)
created_at
updated_at
support_messages
id - primary key (UUID recommended)
chat_id - foreign key to support_chats
content - text (required)
sender_type - enum: 'user' | 'admin'
read_at - timestamp (null = unread)
created_at
updated_at
主要なインデックス:
support_chats.user_id(unique)support_chats.last_message_at(並べ替え用)support_chats.archived_at(フィルタリング用)support_messages.chat_idsupport_messages.(chat_id, created_at)(複合、順序付け用)
モデルのリレーション:
User has_one SupportChat
SupportChat belongs_to User
SupportChat has_many SupportMessages
SupportMessage belongs_to SupportChat
実装するモデルメソッド:
Chat モデル:
function touch_last_message()
update last_message_at = now()
function unread_for_admin?()
return exists message where sender_type = 'user'
and created_at > admin_viewed_at
function mark_viewed_by_admin()
update admin_viewed_at = now()
function archive()
update archived_at = now()
function unarchive()
update archived_at = null
function archived?()
return archived_at != null
Message モデル:
after_create:
chat.touch_last_message()
if sender_type == 'user' and chat.archived?:
chat.unarchive() // Auto-reactivate on new user message
after_create_commit:
broadcast_to_chat_channel(message_data)
if sender_type == 'user':
broadcast_to_admin_notification_channel(message_data, chat_info)
if sender_type == 'admin':
schedule_email_notification(delay: 5.minutes)
ステップ 2: API エンドポイント
ユーザー向け:
GET /support_chat - Get or create user's chat with messages
PATCH /support_chat/mark_read - Mark admin messages as read
管理者向け:
GET /admin/chats - List chats (query: archived=true/false)
GET /admin/chats/:id - Get chat with messages
POST /admin/chats/:id/archive - Archive chat
POST /admin/chats/:id/unarchive - Restore chat
コントローラーのロジック:
ユーザー GET /support_chat:
function show()
chat = current_user.support_chat || create_chat(user: current_user)
return {
id: chat.id,
messages: chat.messages.map(m => serialize_message(m))
}
管理者 GET /admin/chats:
function index()
chats = SupportChat
.where(archived_at: params.archived ? not_null : null)
.includes(:user, :messages)
.order(last_message_at: desc)
return chats.map(c => {
id: c.id,
user_email: c.user.email,
last_message_preview: c.messages.last?.content.truncate(100),
last_message_sender: c.messages.last?.sender_type,
message_count: c.messages.count,
unread: c.unread_for_admin?,
archived: c.archived?
})
ステップ 3: WebSocket チャネル
リアルタイム通信用に 2 つのチャネルを作成します。
ChatChannel (各チャットに固有):
class ChatChannel
on_subscribe(chat_id):
chat = find_chat(chat_id)
if not authorized(chat):
reject()
return
stream_from "support_chat:#{chat_id}"
function authorized(chat):
return chat.user_id == current_user.id OR current_user.is_admin
action send_message(content):
if content.blank: return
sender_type = current_user.is_admin ? 'admin' : 'user'
chat.messages.create(content: content, sender_type: sender_type)
AdminNotificationChannel (すべての管理者向けのグローバル):
class AdminNotificationChannel
on_subscribe:
if not current_user.is_admin:
reject()
return
stream_from "admin_support_notifications"
ブロードキャスト (Message モデルから):
function broadcast_message():
message_data = {
id: id,
content: content,
sender_type: sender_type,
read_at: read_at,
created_at: created_at
}
// Broadcast to chat subscribers (user + any viewing admins)
broadcast("support_chat:#{chat.id}", {
type: "new_message",
message: message_data
})
// Notify all admins when user sends message
if sender_type == 'user':
broadcast("admin_support_notifications", {
type: "new_user_message",
chat_id: chat.id,
user_email: chat.user.email,
message: message_data
})
ステップ 4: フロントエンド - ユーザーウィジェット
次のコンポーネントを備えたフローティングチャットウィジェットを作成します:
コンポーネント構成:
ChatWidget (root container)
├── ChatButton (fixed position, bottom-right)
│ ├── Icon (message bubble when closed, X when open)
│ └── UnreadBadge (shows count, caps at "9+")
└── ChatPanel (slides up when open)
├── Header (title + connection status dot)
├── MessageList (scrollable)
│ └── MessageBubble (styled by sender_type)
└── InputArea
├── Textarea (auto-expanding)
└── SendButton
状態管理フック:
function useSupportChat():
state:
chat: Chat | null
connected: boolean
loading: boolean
refs:
consumer: WebSocketConsumer
subscription: ChannelSubscription
seenMessageIds: Set<string> // For deduplication
on_mount:
fetch('/support_chat')
.then(data => {
chat = data
seenMessageIds.addAll(data.messages.map(m => m.id))
})
when chat.id changes:
subscription = consumer.subscribe('ChatChannel', { chat_id: chat.id })
subscription.on_received(data => {
if data.type == 'new_message':
if seenMessageIds.has(data.message.id): return // Dedupe
seenMessageIds.add(data.message.id)
chat.messages.push(data.message)
if data.message.sender_type == 'admin':
play_notification_sound()
})
subscription.on_connected(() => connected = true)
subscription.on_disconnected(() => connected = false)
on_unmount:
subscription.unsubscribe()
function sendMessage(content):
subscription.perform('send_message', { content: content.trim() })
function markAsRead():
fetch('/support_chat/mark_read', { method: 'PATCH' })
// Update local state to mark admin messages as read
return { chat, connected, loading, sendMessage, markAsRead }
ウィジェットの動作:
- 右下隅にフローティングボタンを表示する (固定位置)
- 未読件数バッジを表示する (sender_type='admin' かつ read_at=null のメッセージをカウント)
- ボタンのクリックでパネルを開閉する
- パネルを開いたときに markAsRead() を自動的に呼び出す
- 新しいメッセージが届いたときに自動的に最下部までスクロールする
- 接続状態インジケーターを表示する (緑のドット = 接続済み)
- キーボード: Enter で送信、Shift+Enter で改行
メッセージのスタイリング:
- ユーザーのメッセージ: 右揃え、プライマリカラーの背景
- 管理者のメッセージ: 左揃え、セカンダリ/ミュートの背景
- 各メッセージにタイムスタンプを表示する
ステップ 5: フロントエンド - 管理ダッシュボード
チャット一覧とチャット詳細の 2 つのページを作成します。
チャット一覧ページ:
Header: "Support Chats"
Tabs: [Active] [Archived]
Chat cards (sorted by last_message_at desc):
┌─────────────────────────────────────────┐
│ [Unread indicator] [email protected] │
│ Last message preview text... │
│ 5 messages · 2 minutes ago │
└─────────────────────────────────────────┘
機能:
- タブによるフィルタリング (アクティブとアーカイブ)
- 未読インジケーター (枠線またはバッジを強調表示)
- クリックで詳細へ遷移
- 最後のメッセージが管理者からのものである場合は「You: 」プレフィックスを表示する
チャット詳細ページ:
Header: [email protected] [Archive/Restore button]
Back link
Messages (grouped by date):
──── Monday, January 29 ────
[User bubble] Message content
10:30 AM
[Admin bubble] Reply content
10:35 AM
Input area (same as widget)
機能:
- 区切り線を入れて日付ごとにメッセージをグループ化する
- ユーザーのメッセージは左、管理者のメッセージは右 (ユーザーウィジェットとは逆)
- 送信者ラベルを表示する (管理者には「You」、ユーザーにはユーザーのメールアドレス/名前)
- アーカイブ/復元の切り替えボタン
- リアルタイム更新のためにユーザーウィジェットと同じ WebSocket サブスクリプション
- ページの読み込み時に mark_viewed_by_admin() を呼び出す (サーバー側)
ステップ 6: メール通知
管理者が返信し、ユーザーがまだそれを確認していない場合に、ユーザーにメールを送信します。
ジョブ/ワーカー:
class SupportReplyNotificationJob
perform(message):
if message.sender_type != 'admin': return
if message.read_at != null: return // Already read, skip
send_email(
to: message.chat.user.email,
subject: "New reply from Support",
body: "You have a new message from our support team..."
)
スケジューリング:
- 管理者がメッセージを送信したときに 5 分遅延でジョブをスケジュールする
- これにより、ユーザーがメールの前にアプリ内でメッセージを確認する時間を確保できる
- ジョブは送信前にまだ未読かどうかを確認する
ステップ 7: TypeScript の型
interface SupportMessage {
id: string
content: string
sender_type: 'user' | 'admin'
read_at: string | null // ISO8601
created_at: string // ISO8601
}
interface SupportChat {
id: string
messages: SupportMessage[]
}
interface SupportChatListItem {
id: string
user_id: string
user_email: string
last_message_at: string | null
last_message_preview: string | null
last_message_sender: 'user' | 'admin' | null
message_count: number
unread: boolean
archived: boolean
}
interface AdminSupportChat {
id: string
user_id: string
user_email: string
archived: boolean
messages: SupportMessage[]
}
// WebSocket message types
interface ChatChannelMessage {
type: 'new_message'
message: SupportMessage
}
interface AdminNotificationMessage {
type: 'new_user_message'
chat_id: string
user_email: string
message: SupportMessage
}
主要な設計上の決定
- ユーザーごとに 1 つのチャット - UX を簡素化し、ユーザーは常に同じ会話履歴を持つ
- アーカイブによるソフト削除 - 履歴を保持し、復元を可能にする
- 自動アーカイブ解除 - ユーザーがアーカイブされたチャットにメッセージを送信すると、それを再有効化する
- 遅延メール通知 - 5 分の遅延により、迅速な返信に対するスパムを防ぐ
- メッセージの重複排除 - 送信とブロードキャストのエコーによる重複を防ぐために、確認済み ID を追跡する
- 管理者専用チャネルの分離 - グローバルな未読件数やデスクトップ通知などの将来の機能を可能にする
テストチェックリスト
実装後:
- ユーザーがウィジェットを開いてメッセージを送信できる
- 管理者のダッシュボードにメッセージがリアルタイムで表示される
- 管理者が返信でき、ユーザー側に即座に表示される
- 未読バッジに正しい件数が表示される
- ウィジェットを開くとバッジが消える
- 接続インジケーターが実際のステータスを反映する
- アーカイブ/復元が正しく動作する
- ユーザーのメッセージ送信時に自動でアーカイブが解除される
- メッセージが未読の場合、5 分後にメールが送信される
- ユーザーがすでにメッセージを読んでいる場合、メールが送信されない
- メッセージが時系列順に表示される
- メッセージが重複して表示されない
よくある落とし穴
- 重複排除を忘れる - 現在のユーザーが送信したメッセージは、ブロードキャスト経由で返ってきます
- 既読ステータスの競合状態 - データベーストランザクションを使用してください
- WebSocket 認証 - ユーザーが特定のチャットにアクセスできることを確認してください
- 接続ステータスが古い - 再接続を適切に処理してください
- インデックスの不足 - (chat_id, created_at) に複合インデックスを追加してください
- メールの送信タイミング - 同期送信ではなく、バックグラウンドジョブを使用してください
フレームワーク別のガイダンス
Ruby on Rails
モデル:
# app/models/support_chat.rb
class SupportChat < ApplicationRecord
belongs_to :user
has_many :support_messages, dependent: :destroy
scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) }
scope :recent_first, -> { order(last_message_at: :desc) }
def touch_last_message
update_column(:last_message_at, Time.current)
end
def unread_for_admin?
support_messages.where(sender_type: :user)
.where("created_at > ?", admin_viewed_at || Time.at(0)).exists?
end
def archive!
update_column(:archived_at, Time.current)
end
def unarchive!
update_column(:archived_at, nil)
end
end
# app/models/support_message.rb
class SupportMessage < ApplicationRecord
belongs_to :support_chat
enum :sender_type, { user: 0, admin: 1 }
validates :content, presence: true
after_create :update_chat_timestamp
after_create :auto_unarchive, if: :user?
after_create_commit :broadcast_message
after_create_commit :schedule_notification, if: :admin?
private
def broadcast_message
ActionCable.server.broadcast("support_chat:#{support_chat_id}", {
type: "new_message",
message: { id:, content:, sender_type:, read_at:, created_at: }
})
end
def schedule_notification
SupportReplyNotificationJob.set(wait: 5.minutes).perform_later(self)
end
end
チャンネル:
# app/channels/support_chat_channel.rb
class SupportChatChannel < ApplicationCable::Channel
def subscribed
@chat = SupportChat.find(params[:chat_id])
reject unless @chat.user_id == current_user.id || current_user.admin?
stream_from "support_chat:#{@chat.id}"
end
def send_message(data)
@chat.support_messages.create!(
content: data["content"],
sender_type: current_user.admin? ? :admin : :user
)
end
end
マイグレーション:
create_table :support_chats, id: :uuid do |t|
t.references :user, type: :uuid, null: false, foreign_key: true, index: { unique: true }
t.datetime :last_message_at
t.datetime :admin_viewed_at
t.datetime :archived_at
t.timestamps
end
create_table :support_messages, id: :uuid do |t|
t.references :support_chat, type: :uuid, null: false, foreign_key: true
t.text :content, null: false
t.integer :sender_type, default: 0
t.datetime :read_at
t.timestamps
end
add_index :support_messages, [:support_chat_id, :created_at]
React(任意のバックエンドと併用)
フック:
// hooks/useSupportChat.ts
import { useEffect, useState, useRef, useCallback } from 'react'
export function useSupportChat(websocketUrl: string) {
const [chat, setChat] = useState<Chat | null>(null)
const [connected, setConnected] = useState(false)
const wsRef = useRef<WebSocket | null>(null)
const seenIds = useRef(new Set<string>())
useEffect(() => {
fetch('/api/support_chat').then(r => r.json()).then(data => {
setChat(data)
data.messages.forEach((m: Message) => seenIds.current.add(m.id))
})
}, [])
useEffect(() => {
if (!chat?.id) return
const ws = new WebSocket(`${websocketUrl}?chat_id=${chat.id}`)
wsRef.current = ws
ws.onopen = () => setConnected(true)
ws.onclose = () => setConnected(false)
ws.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'new_message' && !seenIds.current.has(data.message.id)) {
seenIds.current.add(data.message.id)
setChat(prev => prev ? { ...prev, messages: [...prev.messages, data.message] } : prev)
}
}
return () => ws.close()
}, [chat?.id])
const sendMessage = useCallback((content: string) => {
wsRef.current?.send(JSON.stringify({ action: 'send_message', content }))
}, [])
return { chat, connected, sendMessage }
}
ウィジェットコンポーネント:
// components/ChatWidget.tsx
export function ChatWidget() {
const [isOpen, setIsOpen] = useState(false)
const { chat, connected, sendMessage } = useSupportChat('/ws/chat')
const [input, setInput] = useState('')
const messagesEndRef = useRef<HTMLDivElement>(null)
const unreadCount = chat?.messages.filter(
m => m.sender_type === 'admin' && !m.read_at
).length ?? 0
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
}, [chat?.messages])
const handleSend = () => {
if (!input.trim()) return
sendMessage(input.trim())
setInput('')
}
return (
<div className="fixed bottom-4 right-4 z-50">
{isOpen ? (
<div className="w-80 h-96 bg-white rounded-lg shadow-xl flex flex-col">
<header className="p-3 border-b flex justify-between items-center">
<span>Support Chat</span>
<span className={`w-2 h-2 rounded-full ${connected ? 'bg-green-500' : 'bg-gray-400'}`} />
</header>
<div className="flex-1 overflow-y-auto p-3 space-y-2">
{chat?.messages.map(m => (
<div key={m.id} className={`p-2 rounded ${m.sender_type === 'user' ? 'bg-blue-100 ml-auto' : 'bg-gray-100'}`}>
{m.content}
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="p-3 border-t flex gap-2">
<input value={input} onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleSend()}
className="flex-1 border rounded px-2" placeholder="Type a message..." />
<button onClick={handleSend} className="px-3 py-1 bg-blue-500 text-white rounded">Send</button>
</div>
</div>
) : (
<button onClick={() => setIsOpen(true)} className="w-14 h-14 bg-blue-500 rounded-full text-white relative">
💬
{unreadCount > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-xs w-5 h-5 rounded-full flex items-center justify-center">
{unreadCount > 9 ? '9+' : unreadCount}
</span>
)}
</button>
)}
</div>
)
}
Next.js(App Router)
API ルート:
// app/api/support-chat/route.ts
import { getServerSession } from 'next-auth'
import { prisma } from '@/lib/prisma'
export async function GET() {
const session = await getServerSession()
if (!session?.user) return Response.json({ error: 'Unauthorized' }, { status: 401 })
let chat = await prisma.supportChat.findUnique({
where: { userId: session.user.id },
include: { messages: { orderBy: { createdAt: 'asc' } } }
})
if (!chat) {
chat = await prisma.supportChat.create({
data: { userId: session.user.id },
include: { messages: true }
})
}
return Response.json(chat)
}
Pusher/Ably を使用した WebSocket(サーバーレスに適しています):
// For serverless, use Pusher, Ably, or similar
import Pusher from 'pusher'
const pusher = new Pusher({ appId, key, secret, cluster })
// When message is created:
await pusher.trigger(`support-chat-${chatId}`, 'new-message', messageData)
// Client-side with pusher-js:
const channel = pusher.subscribe(`support-chat-${chatId}`)
channel.bind('new-message', (data) => { /* update state */ })
PHP/Laravel
モデル:
// app/Models/SupportChat.php
class SupportChat extends Model
{
protected $casts = ['last_message_at' => 'datetime', 'archived_at' => 'datetime'];
public function user() { return $this->belongsTo(User::class); }
public function messages() { return $this->hasMany(SupportMessage::class); }
public function scopeActive($query) { return $query->whereNull('archived_at'); }
public function scopeArchived($query) { return $query->whereNotNull('archived_at'); }
public function isUnreadForAdmin(): bool {
return $this->messages()
->where('sender_type', 'user')
->where('created_at', '>', $this->admin_viewed_at ?? '1970-01-01')
->exists();
}
}
// app/Models/SupportMessage.php
class SupportMessage extends Model
{
protected static function booted() {
static::created(function ($message) {
$message->supportChat->update(['last_message_at' => now()]);
broadcast(new NewSupportMessage($message))->toOthers();
if ($message->sender_type === 'admin') {
SendSupportReplyNotification::dispatch($message)->delay(now()->addMinutes(5));
}
});
}
}
ブロードキャストイベント:
// app/Events/NewSupportMessage.php
class NewSupportMessage implements ShouldBroadcast
{
public function __construct(public SupportMessage $message) {}
public function broadcastOn() {
return new PrivateChannel('support-chat.' . $this->message->support_chat_id);
}
public function broadcastAs() { return 'new-message'; }
}
Vue.js
コンポーザブル:
// composables/useSupportChat.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useSupportChat() {
const chat = ref<Chat | null>(null)
const connected = ref(false)
let ws: WebSocket | null = null
const seenIds = new Set<string>()
onMounted(async () => {
const res = await fetch('/api/support-chat')
chat.value = await res.json()
chat.value?.messages.forEach(m => seenIds.add(m.id))
ws = new WebSocket(`/ws/chat?id=${chat.value?.id}`)
ws.onopen = () => connected.value = true
ws.onclose = () => connected.value = false
ws.onmessage = (e) => {
const data = JSON.parse(e.data)
if (data.type === 'new_message' && !seenIds.has(data.message.id)) {
seenIds.add(data.message.id)
chat.value?.messages.push(data.message)
}
}
})
onUnmounted(() => ws?.close())
const sendMessage = (content: string) => {
ws?.send(JSON.stringify({ action: 'send_message', content }))
}
return { chat, connected, sendMessage }
}
データベースに関する推奨事項
PostgreSQL(推奨)
- セキュリティのため、推測できない ID となる UUID の主キーを使用する
- すべての日時カラムに
timestamptzを使用する - 全文検索用に content に GIN インデックスを追加する(任意)
MySQL
- UUID には
CHAR(36)またはBINARY(16)を使用する - マイクロ秒精度には
DATETIME(6)を使用する - 絵文字をサポートするため、
utf8mb4文字セットを検討する
SQLite(開発/小規模向け)
- プロトタイピングには十分に使用できます
- UUID は TEXT として保存する
- ネイティブの日時型がないため、ISO8601 文字列として保存する
MongoDB(ドキュメントストア)
- メッセージ数に上限がある場合は、チャットドキュメント内にメッセージを埋め込む
- または、chat_id を参照する別のコレクションを使用する
- アーカイブ済みチャットの自動クリーンアップには、TTL インデックスを使用する(任意)
メール処理に関する推奨事項
トランザクションメールサービス
- Postmark - 最も高い到達率とシンプルな API
- SendGrid - 無料枠が充実し、堅牢
- AWS SES - 大規模運用で最も低コスト
- Resend - モダンな DX と React メールテンプレート
実装パターン
// Always use background jobs for email
Job: SendSupportReplyNotification
delay: 5 minutes after admin message
perform(message_id):
message = find_message(message_id)
// Guard clauses - don't send if:
if message.sender_type != 'admin': return
if message.read_at != null: return // Already read
if message.chat.archived?: return // Chat archived
send_email(
to: message.chat.user.email,
template: 'support_reply',
data: { message_preview: message.content.truncate(200) }
)
メールテンプレートのヒント
- メッセージのプレビューを含める(省略表示)
- チャットを開く直接リンクを追加する(ウェブアプリの場合)
- 件名はシンプルにする:「[App] サポートからの新しい返信」
- コンプライアンス対応のため、配信停止リンクを含める
リアルタイム技術の選択肢
| 技術 | 最適な用途 | サーバーレス対応? |
|---|---|---|
| ActionCable (Rails) | Rails アプリ | いいえ |
| Socket.IO | Node.js アプリ | いいえ |
| Pusher | あらゆるスタック | はい |
| Ably | あらゆるスタック | はい |
| Supabase Realtime | Supabase ユーザー | はい |
| Firebase RTDB | Firebase ユーザー | はい |
| Server-Sent Events | シンプルな単方向通信 | はい |
フォールバック戦略
WebSocket が利用できない場合は、ポーリングを実装します:
// Poll every 5 seconds when disconnected
if (!websocket.connected) {
setInterval(() => {
fetch('/api/support-chat/messages?since=' + lastMessageTime)
.then(newMessages => appendMessages(newMessages))
}, 5000)
}