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 Changes | PostgreSQL | チャットメッセージ、通知、注文ステータス | はい |
| 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
}
いくつか注意すべきポイントがあります:
- 先に履歴を取得、それから増分を購読:チャットルームに入ったら、まず履歴メッセージを表示し、それから新しいメッセージを受信する必要があります。これはよくある落とし穴で、購読だけで履歴を取得しないというミスをしないでください。
- filter パラメータ:データベースのフィールドでフィルタリングし、無関係なメッセージを受信しないようにします。構文は
フィールド名=eq.値です。 - 購読のクリーンアップ:コンポーネントがアンマウントされる時に
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つを確認してください:
- テーブルが
supabase_realtimepublication に追加されているか - 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つの Channel を共有:3つの機能で同じ channel を使用し、接続数を削減。
- Presence データ構造:
is_typingを同じ presence オブジェクトに入れる。 - クリーンアップ作業:コンポーネントがアンマウントされる時に必ず
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 ポリシーを適用し、ユーザーは権限のあるデータの変更だけを受信できます。
サブスクリプションでデータを受信できない場合、確認する順序:
- テーブルが
supabase_realtimepublication に追加されているか - RLS ポリシーが正しいか(Supabase Dashboard の RLS テストツールを使用)
- ユーザーがログインしているか(
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つのモードにはどんな違いがありますか?
• Postgres Changes はデータベースの変更を監視、データは永続化、チャットメッセージや注文ステータスに適している
• Presence はユーザーのオンライン状態を追跡、データはメモリに保存、オンラインリストや入力インジケーターに適している
• Broadcast は一時的なメッセージを配信、保存しない、カーソル追跡やリアルタイム位置に適している
サブスクリプションでデータを受信できないのはなぜですか?
1. テーブルが supabase_realtime publication に追加されているか(ALTER publication supabase_realtime ADD TABLE テーブル名 を実行)
2. RLS ポリシーが正しく設定されているか(Realtime サブスクリプションは自動的に RLS を適用)
3. ユーザーにデータを表示する権限があるか(auth.uid() の戻り値を確認)
Postgres Changes と Broadcast のどちらを選ぶべきですか?
Presence のデータは永続化されますか?
Realtime サブスクリプションは RLS にどのような影響を与えますか?
1つのページで何個の channel を使えますか?
5 min read · 公開日: 2026年4月15日 · 更新日: 2026年4月15日
関連記事
Supabase 入門ガイド:PostgreSQL + Auth + Storage でオールインワン バックエンド
Supabase 入門ガイド:PostgreSQL + Auth + Storage でオールインワン バックエンド
Supabase データベース設計:テーブル構造、リレーションとRLS完全ガイド
Supabase データベース設計:テーブル構造、リレーションとRLS完全ガイド
Supabase Auth 実践ガイド:メール認証、OAuth、セッション管理

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