言語を切り替える
テーマを切り替える

Supabase Realtime 実践ガイド:3つのモード比較とコラボレーションアプリ開発

深夜3時、画面上の「入力中…」という表示を睨みながら、チャットページを17回目の更新をしていました。向こう側の友人は確かにオンラインなのに、メッセージが全く届かない。その時、本当に「リアルタイム」なアプリを作るのは想像以上に難しいことに気づきました。

WebSocket の落とし穴には何度もハマりました。接続が切れたら再接続、状態同期の処理、ブロードキャストメッセージの設計。去年 Supabase Realtime を使い始めて、これらの面倒な作業を誰かに任せられることに気づきました。Supabase は3つのリアルタイムモードを提供しています:Postgres Changes でデータベースの変更を監視、Presence でユーザー状態を追跡、Broadcast で一時的なメッセージを配信。この3つのモードにはそれぞれ適した用途があり、正しく使えば効率的ですが、間違えると自分で穴を掘ることになります。

この記事では、この3つのモードを詳しく解説します。いつどれを使うべきか、コードはどう書くか、RLS セキュリティポリシーはどう設定するか。最後に、それらを組み合わせて完全なコラボレーションチャットアプリを作ります。

Supabase Realtime 3つのコア機能比較

結論から言うと:3つのモードにはそれぞれ異なる役割があり、混同しないことです。

Postgres Changes はデータベースの変更を監視します。チャットメッセージ、通知、注文ステータスなど、永続化が必要なデータに適しています。データはデータベースに保存され、クライアントは変更を購読するだけです。

Presence はユーザーのオンライン状態を追跡します。「誰がオンラインか」、入力インジケーター、コラボレーション編集でのカーソル位置などに適しています。データはデータベースに保存されず、メモリに保存され、ユーザーが切断すると消えます。

Broadcast は一時的なメッセージを配信します。キャンバス上のカーソル移動、ゲーム内のリアルタイム位置、一時的な操作の同期などに適しています。Presence との違いは:Broadcast は高頻度の送信に適しており、Presence は状態の同期に適しています。

1つの表で整理しましょう:

モードデータの保存先典型的なユースケース永続化
Postgres ChangesPostgreSQLチャットメッセージ、通知、注文ステータスはい
Presenceメモリ(Realtime サービス)オンラインユーザー、入力インジケーターいいえ
Broadcast保存しない(即時転送)カーソル移動、リアルタイム位置いいえ

「なぜ全部 Postgres Changes でいいじゃないか」と思うかもしれません。正直に言うと、私も最初はそう思いました。その後、コラボレーションホワイトボードアプリを作り、カーソルの移動ごとにデータベースに書き込んでいたら、データベースの CPU が90%まで上昇しました。その時、一部のデータは永続化する必要がないことに気づきました。

どのモードを選ぶべきか、このシンプルな原則を覚えておいてください:履歴を確認する必要があるなら Postgres Changes、現在の状態だけに関心があるなら Presence、高頻度かつ一時的ななら Broadcast。

Postgres Changes:データベースの変更を監視

このセクションでは最も一般的なシナリオを解説します:データベースの変更を監視すること。例えば、チャットルームに新しいメッセージが来た、注文ステータスが変わった、誰かが「いいね」を押した—これらはすべて永続化が必要なデータです。

Supabase Realtime は PostgreSQL の logical replication(論理レプリケーション)メカニズムを通じて変更監視を実現しています。簡単に言うと、データベースで INSERT、UPDATE、DELETE 操作があるたびに、Realtime サービスがそれをキャプチャし、購読しているクライアントにプッシュします。

Realtime 監視の有効化

まずデータベース側で publication を有効にする必要があります。Supabase SQL Editor で実行します:

-- Realtime publication を有効化
ALTER publication supabase_realtime ADD TABLE messages;

-- UPDATE と DELETE を監視する必要があるテーブルでは、REPLICA IDENTITY FULL を設定する必要がある
ALTER TABLE messages REPLICA IDENTITY FULL;

なぜ REPLICA IDENTITY FULL を設定する必要があるのでしょうか?デフォルトでは、PostgreSQL は変更された行の主キーだけを記録します。変更前後の完全なデータを取得したい場合(例えば監査ログを作る場合)、このオプションを有効にする必要があります。注意:データベースの書き込み量が増えるので、本当に必要なテーブルだけに有効にしてください。

クライアントのサブスクリプションコード

次はクライアント側のコードです。チャットメッセージを例にします:

import { createClient } from '@supabase/supabase-js'
import { useEffect, useState } from 'react'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

interface Message {
  id: string
  content: string
  user_id: string
  created_at: string
}

export function useRealtimeMessages(roomId: string) {
  const [messages, setMessages] = useState<Message[]>([])

  useEffect(() => {
    // まず履歴メッセージを取得
    const fetchMessages = async () => {
      const { data } = await supabase
        .from('messages')
        .select('*')
        .eq('room_id', roomId)
        .order('created_at', { ascending: true })

      if (data) setMessages(data)
    }

    fetchMessages()

    // 新しいメッセージを購読
    const channel = supabase
      .channel(`messages:${roomId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',      // 新規追加のみ監視
          schema: 'public',
          table: 'messages',
          filter: `room_id=eq.${roomId}`  // 特定のルームでフィルタ
        },
        (payload) => {
          // payload.new には新しく挿入されたデータが含まれる
          setMessages(prev => [...prev, payload.new as Message])
        }
      )
      .subscribe()

    // 購読のクリーンアップ
    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId])

  return messages
}

いくつか注意すべきポイントがあります:

  1. 先に履歴を取得、それから増分を購読:チャットルームに入ったら、まず履歴メッセージを表示し、それから新しいメッセージを受信する必要があります。これはよくある落とし穴で、購読だけで履歴を取得しないというミスをしないでください。
  2. filter パラメータ:データベースのフィールドでフィルタリングし、無関係なメッセージを受信しないようにします。構文は フィールド名=eq.値 です。
  3. 購読のクリーンアップ:コンポーネントがアンマウントされる時に removeChannel を忘れないでください。そうしないとメモリリークが発生します。

RLS セキュリティ設定(重要!)

この部分は多くの人が無視しがちですが、本番アプリケーションの重要な要素です。デフォルトでは、Realtime はテーブルの RLS(Row Level Security)ポリシーに従います。RLS を設定していないと、クライアントは何も受信できないか、受信すべきでないデータを受信してしまう可能性があります。

例えば、チャットルームのテーブルがあるとします:

-- メッセージテーブル
CREATE TABLE messages (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  room_id uuid REFERENCES rooms(id),
  user_id uuid REFERENCES auth.users(id),
  content text NOT NULL,
  created_at timestamptz DEFAULT now()
);

-- RLS を有効化
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;

-- 自分が所属するルームのメッセージを表示することを許可
CREATE POLICY "Users can view messages in their rooms"
ON messages FOR SELECT
USING (
  room_id IN (
    SELECT room_id FROM room_members
    WHERE user_id = auth.uid()
  )
);

-- ルームメンバーがメッセージを送信することを許可
CREATE POLICY "Room members can insert messages"
ON messages FOR INSERT
WITH CHECK (
  room_id IN (
    SELECT room_id FROM room_members
    WHERE user_id = auth.uid()
  )
);

Realtime サブスクリプションはこれらのポリシーを自動的に適用します。ユーザーは権限のあるメッセージの変更だけを受信できます。これは非常に重要です—フロントエンドでフィルタリングしようと考えないでください、それは安全ではありません。

サブスクリプションでデータを受信できない場合、まず2つを確認してください:

  1. テーブルが supabase_realtime publication に追加されているか
  2. RLS ポリシーが正しく設定されているか

Presence:ユーザーのオンライン状態を追跡

Presence は「今誰がオンラインか」というシナリオに適しています—チャットルームのオンライン人数表示、コラボレーションドキュメントで誰がどの部分を編集しているか、誰が入力しているか。これらのデータはデータベースに保存する必要がなく、メモリに保存するだけで十分です。

基本的な仕組み

Presence の仕組みは:各クライアントがチャンネルに参加した後、track() メソッドを呼び出して自分の状態を登録します。Realtime サービスは全クライアントの状態のスナップショットを維持し、誰かが参加、離脱、状態を更新すると、全てのサブスクライバーに通知が送られます。

オンラインユーザーリストの実装

コードを見てみましょう。チャットルームのオンラインユーザーを表示するコンポーネントです:

import { createClient } from '@supabase/supabase-js'
import { useEffect, useState } from 'react'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

interface UserPresence {
  user_id: string
  username: string
  online_at: string
}

export function useOnlineUsers(roomId: string, currentUser: { id: string; username: string }) {
  const [users, setUsers] = useState<UserPresence[]>([])

  useEffect(() => {
    const channel = supabase.channel(`room:${roomId}`, {
      config: {
        presence: {
          key: currentUser.id  // ユーザー ID をキーとして使用
        }
      }
    })

    channel
      .on('presence', { event: 'sync' }, () => {
        // 同期時に全オンラインユーザーを取得
        const state = channel.presenceState()
        // presenceState() は { [key]: [UserPresence, ...] } を返す
        const onlineUsers = Object.values(state).flat() as UserPresence[]
        setUsers(onlineUsers)
      })
      .on('presence', { event: 'join' }, ({ newPresences }) => {
        // 新しいユーザーが参加
        console.log('ユーザー参加:', newPresences)
      })
      .on('presence', { event: 'leave' }, ({ leftPresences }) => {
        // ユーザーが離脱
        console.log('ユーザー離脱:', leftPresences)
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          // 購読成功後、自分の状態を登録
          await channel.track({
            user_id: currentUser.id,
            username: currentUser.username,
            online_at: new Date().toISOString()
          })
        }
      })

    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId, currentUser])

  return users
}

使い方は簡単です:

function ChatRoom({ roomId, currentUser }) {
  const onlineUsers = useOnlineUsers(roomId, currentUser)

  return (
    <div className="flex items-center gap-2 mb-4">
      <span className="text-sm text-gray-500">
        {onlineUsers.length} 人がオンライン
      </span>
      <div className="flex -space-x-2">
        {onlineUsers.map(user => (
          <div
            key={user.user_id}
            className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-sm"
            title={user.username}
          >
            {user.username[0]}
          </div>
        ))}
      </div>
    </div>
  )
}

入力インジケーター

Presence は「入力中」の表示にも使えます。アイデアは:ユーザーが入力を開始したら自分の状態を更新し、入力を止めてからしばらくしたら状態をクリアします。

export function useTypingIndicator(roomId: string, currentUser: { id: string }) {
  const [typingUsers, setTypingUsers] = useState<string[]>([])
  const channelRef = useRef<RealtimeChannel | null>(null)
  const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null)

  useEffect(() => {
    const channel = supabase.channel(`room:${roomId}`)
    channelRef.current = channel

    channel
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState()
        const typing = Object.values(state)
          .flat()
          .filter((u: any) => u.is_typing && u.user_id !== currentUser.id)
          .map((u: any) => u.username)
        setTypingUsers(typing)
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          await channel.track({
            user_id: currentUser.id,
            is_typing: false
          })
        }
      })

    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId, currentUser])

  // ユーザーが入力を開始した時に呼び出す
  const setTyping = (isTyping: boolean) => {
    if (typingTimeoutRef.current) {
      clearTimeout(typingTimeoutRef.current)
    }

    channelRef.current?.track({
      user_id: currentUser.id,
      is_typing
    })

    // 3秒後に自動的にタイピング状態をクリア
    if (isTyping) {
      typingTimeoutRef.current = setTimeout(() => {
        channelRef.current?.track({
          user_id: currentUser.id,
          is_typing: false
        })
      }, 3000)
    }
  }

  return { typingUsers, setTyping }
}

ここに小さな落とし穴があります:すべてのキー操作で track() を呼び出さないでください。頻繁すぎると問題が発生します。デバウンスを追加するか、上記のように、入力が終わって数秒後に自動的に状態をクリアしてください。

Broadcast:カーソル追跡と即時メッセージ

Broadcast は3つのモードの中で最も「軽量」です—メッセージは保存されず、永続化もされず、送信したら忘れる、高頻度の一時的なデータ転送に適しています。

カーソル追跡の例

コラボレーションホワイトボード、複数人エディターのようなアプリでは、各人のカーソル位置をリアルタイムで表示する必要があります。このシナリオには Broadcast が最適です—カーソル位置はデータベースに保存する必要がなく、「過去のカーソル位置」を知る必要もありません。

import { createClient } from '@supabase/supabase-js'
import { useEffect, useState, useRef } from 'react'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

interface CursorPosition {
  user_id: string
  username: string
  x: number
  y: number
  color: string
}

export function useCursors(canvasId: string, currentUser: { id: string; username: string }) {
  const [cursors, setCursors] = useState<Record<string, CursorPosition>>({})
  const channelRef = useRef<RealtimeChannel | null>(null)

  useEffect(() => {
    const channel = supabase.channel(`canvas:${canvasId}`)
    channelRef.current = channel

    channel
      .on('broadcast', { event: 'cursor' }, ({ payload }) => {
        // 他のユーザーのカーソル位置を受信
        if (payload.user_id !== currentUser.id) {
          setCursors(prev => ({
            ...prev,
            [payload.user_id]: payload
          }))
        }
      })
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [canvasId, currentUser])

  // 自分のカーソル位置を送信
  const sendCursor = (x: number, y: number) => {
    channelRef.current?.send({
      type: 'broadcast',
      event: 'cursor',
      payload: {
        user_id: currentUser.id,
        username: currentUser.username,
        x,
        y,
        color: getUserColor(currentUser.id)
      }
    })
  }

  return { cursors, sendCursor }
}

// ユーザー ID に基づいて色を生成
function getUserColor(userId: string): string {
  const colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4']
  const index = userId.charCodeAt(0) % colors.length
  return colors[index]
}

コンポーネントで使用:

function CollaborativeCanvas({ canvasId, currentUser }) {
  const { cursors, sendCursor } = useCursors(canvasId, currentUser)

  const handleMouseMove = (e: React.MouseEvent) => {
    sendCursor(e.clientX, e.clientY)
  }

  return (
    <div className="relative w-full h-full" onMouseMove={handleMouseMove}>
      {/* 他のユーザーのカーソルを表示 */}
      {Object.entries(cursors).map(([userId, cursor]) => (
        <div
          key={userId}
          className="absolute pointer-events-none"
          style={{ left: cursor.x, top: cursor.y }}
        >
          <div className="w-4 h-4 rounded-full" style={{ backgroundColor: cursor.color }} />
          <span className="text-xs ml-1">{cursor.username}</span>
        </div>
      ))}
    </div>
  )
}

Broadcast vs Presence

Broadcast と Presence には重複があることに気づくかもしれません。違いは:

  • Broadcast は高頻度の送信(毎秒数十回の可能性)に使用、例えばカーソル移動、ゲーム内の位置同期
  • Presence は状態の同期(たまに更新)に使用、例えば「誰がオンラインか」、「入力中」

両方を組み合わせて使うのがベストです。Broadcast は「動く」データに、Presence は「状態」データに適しています。

総合実践:コラボレーションチャットアプリの構築

では、3つのモードを組み合わせて、本当のコラボレーションチャットアプリを作ってみましょう。機能は以下の通り:

  • リアルタイムメッセージ(Postgres Changes)
  • オンラインユーザーリスト(Presence)
  • 入力インジケーター(Presence)

データベースの準備

まずテーブルを作成:

-- ルームテーブル
CREATE TABLE rooms (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  name text NOT NULL,
  created_at timestamptz DEFAULT now()
);

-- ルームメンバーテーブル
CREATE TABLE room_members (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  room_id uuid REFERENCES rooms(id) ON DELETE CASCADE,
  user_id uuid REFERENCES auth.users(id),
  joined_at timestamptz DEFAULT now(),
  UNIQUE(room_id, user_id)
);

-- メッセージテーブル
CREATE TABLE messages (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  room_id uuid REFERENCES rooms(id) ON DELETE CASCADE,
  user_id uuid REFERENCES auth.users(id),
  content text NOT NULL,
  created_at timestamptz DEFAULT now()
);

-- Realtime を有効化
ALTER publication supabase_realtime ADD TABLE messages;

-- RLS を有効化
ALTER TABLE messages ENABLE ROW LEVEL Security;

-- RLS ポリシー:自分が所属するルームのメッセージだけを見ることができる
CREATE POLICY "Users can view messages in their rooms"
ON messages FOR SELECT
USING (
  room_id IN (
    SELECT room_id FROM room_members WHERE user_id = auth.uid()
  )
);

-- RLS ポリシー:ルームメンバーだけがメッセージを送信できる
CREATE POLICY "Room members can send messages"
ON messages FOR INSERT
WITH CHECK (
  room_id IN (
    SELECT room_id FROM room_members WHERE user_id = auth.uid()
  )
);

完全な React コンポーネント

これは簡略版のコラボレーションチャットコンポーネントで、3つのモードをすべて使用しています:

import { createClient } from '@supabase/supabase-js'
import { useEffect, useState, useRef, useCallback } from 'react'

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)

interface Message {
  id: string
  content: string
  user_id: string
  username: string
  created_at: string
}

interface UserPresence {
  user_id: string
  username: string
  is_typing?: boolean
}

export function CollaborativeChat({ roomId, currentUser }) {
  const [messages, setMessages] = useState<Message[]>([])
  const [onlineUsers, setOnlineUsers] = useState<UserPresence[]>([])
  const [typingUsers, setTypingUsers] = useState<string[]>([])
  const [inputValue, setInputValue] = useState('')

  const channelRef = useRef<RealtimeChannel | null>(null)
  const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null)

  useEffect(() => {
    // 1つの channel を作成、3つの機能を共有
    const channel = supabase.channel(`room:${roomId}`, {
      config: { presence: { key: currentUser.id } }
    })
    channelRef.current = channel

    // 1. Postgres Changes:新しいメッセージを監視
    channel.on(
      'postgres_changes',
      {
        event: 'INSERT',
        schema: 'public',
        table: 'messages',
        filter: `room_id=eq.${roomId}`
      },
      (payload) => setMessages(prev => [...prev, payload.new as Message])
    )

    // 2. Presence:オンラインユーザーとタイピング状態を監視
    channel.on('presence', { event: 'sync' }, () => {
      const state = channel.presenceState()
      const users = Object.values(state).flat() as UserPresence[]
      setOnlineUsers(users)

      const typing = users
        .filter(u => u.is_typing && u.user_id !== currentUser.id)
        .map(u => u.username)
      setTypingUsers(typing)
    })

    // 購読して Presence を登録
    channel.subscribe(async (status) => {
      if (status === 'SUBSCRIBED') {
        await channel.track({
          user_id: currentUser.id,
          username: currentUser.username,
          is_typing: false
        })

        // 履歴メッセージをロード
        const { data } = await supabase
          .from('messages')
          .select('*')
          .eq('room_id', roomId)
          .order('created_at', { ascending: true })

        if (data) setMessages(data)
      }
    })

    return () => supabase.removeChannel(channel)
  }, [roomId, currentUser])

  const sendMessage = async () => {
    if (!inputValue.trim()) return
    await supabase.from('messages').insert({
      room_id: roomId,
      user_id: currentUser.id,
      content: inputValue.trim()
    })
    setInputValue('')
    setTyping(false)
  }

  const setTyping = useCallback((isTyping: boolean) => {
    if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current)
    channelRef.current?.track({
      user_id: currentUser.id,
      username: currentUser.username,
      is_typing
    })
    if (isTyping) {
      typingTimeoutRef.current = setTimeout(() => setTyping(false), 3000)
    }
  }, [currentUser])

  return (
    <div className="flex h-full">
      {/* 左側:オンラインユーザー */}
      <div className="w-64 border-r p-4">
        <h3 className="font-bold mb-2">オンライン ({onlineUsers.length})</h3>
        {onlineUsers.map(user => (
          <div key={user.user_id} className="py-1">
            {user.username}
            {user.is_typing && <span className="text-sm text-gray-500"> (入力中)</span>}
          </div>
        ))}
      </div>

      {/* 右側:チャット */}
      <div className="flex-1 flex flex-col">
        <div className="flex-1 overflow-y-auto p-4">
          {messages.map(msg => (
            <div key={msg.id} className="mb-2">
              <span className="font-bold">{msg.username}: </span>
              {msg.content}
            </div>
          ))}
          {typingUsers.length > 0 && (
            <div className="text-gray-500 text-sm">
              {typingUsers.join(', ')} が入力中...
            </div>
          )}
        </div>

        <div className="p-4 border-t">
          <input
            value={inputValue}
            onChange={e => { setInputValue(e.target.value); setTyping(true) }}
            onKeyDown={e => e.key === 'Enter' && sendMessage()}
            placeholder="メッセージを入力..."
            className="w-full p-2 border rounded"
          />
        </div>
      </div>
    </div>
  )
}

重要な設計ポイント

  1. 1つの Channel を共有:3つの機能で同じ channel を使用し、接続数を削減。
  2. Presence データ構造is_typing を同じ presence オブジェクトに入れる。
  3. クリーンアップ作業:コンポーネントがアンマウントされる時に必ず removeChannel を実行。

パフォーマンスとセキュリティのベストプラクティス

ここまでコードは動くようになりましたが、本番に出す前にまだ処理すべき詳細があります。

接続管理

各 channel は1つの WebSocket 接続を占有します。Supabase は複数のチャンネルで1つの接続を共有できますが、濫用すると問題が発生します。

推奨事項

  • 1ページで最大2-3個の channel
  • 関連する機能は1つの channel を共有(上記のチャットの例のように)
  • ページを離れたらすぐに removeChannel
// クリーンアップの例
useEffect(() => {
  const channel = supabase.channel('my-channel')
  channel.subscribe()

  return () => {
    // unsubscribe だけでなく、removeChannel を使うこと
    supabase.removeChannel(channel)
  }
}, [])

RLS は必須

「フロントエンドでフィルタリングすればいい」と考えないでください。Realtime サブスクリプションは自動的に RLS ポリシーを適用し、ユーザーは権限のあるデータの変更だけを受信できます。

サブスクリプションでデータを受信できない場合、確認する順序:

  1. テーブルが supabase_realtime publication に追加されているか
  2. RLS ポリシーが正しいか(Supabase Dashboard の RLS テストツールを使用)
  3. ユーザーがログインしているか(auth.uid() の戻り値を確認)

料金の注意点

Supabase Realtime の料金は同時接続数で計算されます:1000ピーク接続につき $10。ほとんどの中小規模アプリでは、無料枠で十分です。ただし、アプリに大量のユーザーが同時にオンラインになる場合は、channel 数の管理に注意してください。

公式のデモプロジェクト Multiplayer.dev で、3つのモードが実際にどのように動くか体験できます。

まとめ

これだけ説明しましたが、どのモードを選ぶかは実はシンプルです:

履歴を保存する必要がある?高頻度で送信?推奨モード
はいいいえPostgres Changes
いいえいいえPresence
いいえはいBroadcast

コラボレーションアプリ(チャットルーム、ホワイトボード、ドキュメント編集)を作っているなら、3つのモードをすべて使う可能性が高いです。Postgres Changes はメッセージを保存、Presence はオンライン状態を追跡、Broadcast はカーソルを処理します。

まず Multiplayer.dev で遊んでみて、実際の効果を体験することをお勧めします。それから小さなチャットデモを作ってみてください—それが最も早く習得できる方法です。

次回は Supabase Storage、ファイルアップロードと画像処理について書く予定です。Realtime について質問があれば、コメントで議論しましょう。

FAQ

Supabase Realtime の3つのモードにはどんな違いがありますか?
3つのモードにはそれぞれ役割があります:

• Postgres Changes はデータベースの変更を監視、データは永続化、チャットメッセージや注文ステータスに適している
• Presence はユーザーのオンライン状態を追跡、データはメモリに保存、オンラインリストや入力インジケーターに適している
• Broadcast は一時的なメッセージを配信、保存しない、カーソル追跡やリアルタイム位置に適している
サブスクリプションでデータを受信できないのはなぜですか?
3つのポイントを確認してください:

1. テーブルが supabase_realtime publication に追加されているか(ALTER publication supabase_realtime ADD TABLE テーブル名 を実行)
2. RLS ポリシーが正しく設定されているか(Realtime サブスクリプションは自動的に RLS を適用)
3. ユーザーにデータを表示する権限があるか(auth.uid() の戻り値を確認)
Postgres Changes と Broadcast のどちらを選ぶべきですか?
履歴を保存し、遡及をサポートする必要がある場合は Postgres Changes を選んでください。高頻度の一時的データで、永続化が不要な場合は Broadcast を選んでください。例えば、チャットメッセージは前者を、カーソル位置は後者を使用します。
Presence のデータは永続化されますか?
されません。Presence のデータは Realtime サービスのメモリに保存され、ユーザーが切断するとデータは消えます。オンライン状態のような一時的なデータに適しています。
Realtime サブスクリプションは RLS にどのような影響を与えますか?
Realtime サブスクリプションは自動的にテーブルの RLS ポリシーを適用します。ユーザーは権限のあるデータの変更だけを受信できます。これは本番アプリケーションのセキュリティの重要な要素で、フロントエンドでフィルタリングしないでください。
1つのページで何個の channel を使えますか?
最大2-3個をお勧めします。関連する機能は1つの channel を共有(例えばチャットで Postgres Changes + Presence を同時に使用)して、WebSocket 接続数を減らしてください。ページを離れる時は必ず removeChannel を呼び出してクリーンアップしてください。

5 min read · 公開日: 2026年4月15日 · 更新日: 2026年4月15日

シリーズの読書導線 第 6 / 6 記事

Supabase 実践ガイド

検索からこのページに来た場合は、前後の記事もあわせて読むと同じテーマの理解がかなり早く深まります。

シリーズ全体を見る

関連記事

コメント

GitHubアカウントでログインしてコメントできます